Files
frostbyte/src/sfsTest.c
T

1358 lines
43 KiB
C

#include "ktest.h"
#include "sfs.h"
#include "ata.h"
#include "string.h"
#include "heap.h"
#include "console.h"
#include "rtc.h"
#include "fs.h"
/*
* Every test starts with an explicit sfsFormat() so results do not depend on
* whatever build/disk.img happened to contain from a previous `make test`
* run (the disk image is not recreated between runs unless deleted). That
* keeps the gate deterministic: a stale leftover file on disk must never be
* the reason a check passes or fails.
*/
KTEST(sfsFormatAndMountAreReal)
{
if (!KT_TRUE(t, ataPresent()))
return;
KT_TRUE(t, sfsFormat());
KT_TRUE(t, sfsMounted());
KT_GT(t, sfsCapacityBytes(), 1024 * 1024);
int32 root = sfsLookup("");
KT_EQ(t, root, sfsRootInode);
KT_EQ(t, sfsTypeOf(root), sfsTypeDir);
KT_EQ(t, sfsSizeOf(root), 0);
}
KTEST(sfsCreateFileRoundTrip)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("hello.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsTypeOf(f), sfsTypeFile);
KT_EQ(t, sfsSizeOf(f), 0);
const char* msg = "hello, disk!";
uint64 len = strlen(msg);
KT_EQ(t, sfsWriteAt(f, 0, msg, len), (int64)len);
KT_EQ(t, sfsSizeOf(f), len);
char back[32];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(f, 0, back, len), (int64)len);
KT_EQ(t, memcmp(back, msg, len), 0);
}
KTEST(sfsOverwriteReflectsLatestData)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("a.bin");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "AAAA", 4), 4);
KT_EQ(t, sfsWriteAt(f, 0, "BB", 2), 2);
char back[4];
KT_EQ(t, sfsReadAt(f, 0, back, 4), 4);
KT_EQ(t, memcmp(back, "BBAA", 4), 0);
KT_EQ(t, sfsSizeOf(f), 4);
}
KTEST(sfsWritePastEndZeroFillsGap)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("gap.bin");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 10, "X", 1), 1);
KT_EQ(t, sfsSizeOf(f), 11);
uint8 back[11];
KT_EQ(t, sfsReadAt(f, 0, back, 11), 11);
for (int i = 0; i < 10; i++)
KT_EQ(t, back[i], 0);
KT_EQ(t, back[10], 'X');
}
KTEST(sfsMultiBlockFileSpansDirectPointers)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("multi.bin");
KT_TRUE(t, f > 0);
/* Big enough to span several blocks: kmalloc'd rather than kept on the
stack, matching the rbt/slab stress-test convention (this ktest
battery runs on the fixed 16 KiB boot stack, not a per-thread one). */
#define sfsTestSpan (3 * sfsBlockSize + 200)
uint8* pattern = (uint8*)kmalloc(sfsTestSpan);
uint8* back = (uint8*)kmalloc(sfsTestSpan);
KT_NOTNULL(t, pattern);
KT_NOTNULL(t, back);
if (!pattern || !back) {
kfree(pattern);
kfree(back);
return;
}
for (int i = 0; i < sfsTestSpan; i++)
pattern[i] = (uint8)(i * 31 + 7);
KT_EQ(t, sfsWriteAt(f, 0, pattern, sfsTestSpan), sfsTestSpan);
KT_EQ(t, sfsSizeOf(f), sfsTestSpan);
memset(back, 0, sfsTestSpan);
KT_EQ(t, sfsReadAt(f, 0, back, sfsTestSpan), sfsTestSpan);
KT_EQ(t, memcmp(back, pattern, sfsTestSpan), 0);
kfree(pattern);
kfree(back);
#undef sfsTestSpan
}
KTEST(sfsFileGrowsThroughIndirectBlock)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("big.bin");
KT_TRUE(t, f > 0);
/* 10 direct blocks (5120 bytes) plus a chunk that only the single
indirect block can address: this must reach past sfsDirectBlocks.
kmalloc'd (three of these at once would be ~18 KiB, more than the
whole 16 KiB boot stack ktest runs on). */
#define sfsBigSpan (12 * sfsBlockSize + 33)
uint8* pattern = (uint8*)kmalloc(sfsBigSpan);
uint8* back = (uint8*)kmalloc(sfsBigSpan);
uint8* verify = (uint8*)kmalloc(sfsBigSpan);
KT_NOTNULL(t, pattern);
KT_NOTNULL(t, back);
KT_NOTNULL(t, verify);
if (!pattern || !back || !verify) {
kfree(pattern);
kfree(back);
kfree(verify);
return;
}
for (int i = 0; i < sfsBigSpan; i++)
pattern[i] = (uint8)(i ^ 0x5A);
KT_EQ(t, sfsWriteAt(f, 0, pattern, sfsBigSpan), sfsBigSpan);
KT_EQ(t, sfsSizeOf(f), sfsBigSpan);
memset(back, 0, sfsBigSpan);
KT_EQ(t, sfsReadAt(f, 0, back, sfsBigSpan), sfsBigSpan);
KT_EQ(t, memcmp(back, pattern, sfsBigSpan), 0);
/* Overwrite a slice that straddles the direct/indirect boundary and
confirm only that slice changes. */
uint8 patch[64];
memset(patch, 0xEE, sizeof(patch));
uint64 patchOffset = 10 * sfsBlockSize - 20;
KT_EQ(t, sfsWriteAt(f, patchOffset, patch, sizeof(patch)), (int64)sizeof(patch));
KT_EQ(t, sfsReadAt(f, 0, verify, sfsBigSpan), sfsBigSpan);
KT_EQ(t, memcmp(verify + patchOffset, patch, sizeof(patch)), 0);
KT_EQ(t, memcmp(verify, pattern, patchOffset), 0);
kfree(pattern);
kfree(back);
kfree(verify);
#undef sfsBigSpan
}
KTEST(sfsNestedDirectoriesAndLookup)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("etc") > 0);
KT_TRUE(t, sfsCreateDir("etc/sub") > 0);
int32 f = sfsCreateFile("etc/sub/leaf.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "leaf", 4), 4);
int32 viaLookup = sfsLookup("etc/sub/leaf.txt");
KT_EQ(t, viaLookup, f);
int32 subDir = sfsLookup("etc/sub");
KT_TRUE(t, subDir > 0);
KT_EQ(t, sfsTypeOf(subDir), sfsTypeDir);
KT_EQ(t, sfsLookup("etc/nope"), -1);
KT_EQ(t, sfsLookup("nope/at/all"), -1);
}
KTEST(sfsDuplicateCreateRejected)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateFile("dup.txt") > 0);
KT_EQ(t, sfsCreateFile("dup.txt"), -1);
KT_EQ(t, sfsCreateDir("dup.txt"), -1);
}
KTEST(sfsRemoveFileFreesBlocksForReuse)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("tofree.bin");
KT_TRUE(t, f > 0);
#define sfsFreeChunk (4 * sfsBlockSize)
uint8* chunk = (uint8*)kmalloc(sfsFreeChunk);
KT_NOTNULL(t, chunk);
if (!chunk)
return;
memset(chunk, 0x42, sfsFreeChunk);
KT_EQ(t, sfsWriteAt(f, 0, chunk, sfsFreeChunk), sfsFreeChunk);
kfree(chunk);
#undef sfsFreeChunk
KT_TRUE(t, sfsRemove("tofree.bin"));
KT_EQ(t, sfsLookup("tofree.bin"), -1);
/* A same-size file created afterward must succeed, proving the blocks
(and the inode slot) actually came back to the free pool rather than
leaking on every create/remove cycle. */
int32 again = sfsCreateFile("tofree.bin");
KT_TRUE(t, again > 0);
KT_EQ(t, sfsSizeOf(again), 0);
}
KTEST(sfsRmdirRequiresEmpty)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("full") > 0);
KT_TRUE(t, sfsCreateFile("full/child.txt") > 0);
KT_FALSE(t, sfsRemove("full"));
KT_TRUE(t, sfsRemove("full/child.txt"));
KT_TRUE(t, sfsRemove("full"));
KT_EQ(t, sfsLookup("full"), -1);
}
KTEST(sfsReaddirListsAllEntriesAndSwapRemoveKeepsRest)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 dir = sfsCreateDir("listing");
KT_TRUE(t, dir > 0);
KT_TRUE(t, sfsCreateFile("listing/a") > 0);
KT_TRUE(t, sfsCreateFile("listing/b") > 0);
KT_TRUE(t, sfsCreateFile("listing/c") > 0);
uint32 seen = 0;
char name[sfsNameMax + 1];
uint32 type;
for (uint32 i = 0; sfsReaddirAt(dir, i, name, sizeof(name), &type); i++) {
KT_EQ(t, type, sfsTypeFile);
seen++;
}
KT_EQ(t, seen, 3);
KT_TRUE(t, sfsRemove("listing/b"));
KT_NE(t, sfsLookup("listing/a"), -1);
KT_NE(t, sfsLookup("listing/c"), -1);
KT_EQ(t, sfsLookup("listing/b"), -1);
seen = 0;
for (uint32 i = 0; sfsReaddirAt(dir, i, name, sizeof(name), &type); i++)
seen++;
KT_EQ(t, seen, 2);
}
KTEST(sfsTruncateShrinksAndFreesBlocks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("shrink.bin");
KT_TRUE(t, f > 0);
#define sfsShrinkChunk (5 * sfsBlockSize)
uint8* chunk = (uint8*)kmalloc(sfsShrinkChunk);
KT_NOTNULL(t, chunk);
if (!chunk)
return;
memset(chunk, 0x7A, sfsShrinkChunk);
KT_EQ(t, sfsWriteAt(f, 0, chunk, sfsShrinkChunk), sfsShrinkChunk);
KT_TRUE(t, sfsTruncate(f, sfsBlockSize));
KT_EQ(t, sfsSizeOf(f), sfsBlockSize);
uint8 back[sfsBlockSize];
KT_EQ(t, sfsReadAt(f, 0, back, sfsBlockSize), sfsBlockSize);
KT_EQ(t, memcmp(back, chunk, sfsBlockSize), 0);
kfree(chunk);
#undef sfsShrinkChunk
KT_TRUE(t, sfsTruncate(f, 0));
KT_EQ(t, sfsSizeOf(f), 0);
}
KTEST(sfsPersistsAcrossUnmountRemount)
{
/* The real point of this test: sfsUnmount() drops every in-memory
structure (bitmaps freed, mounted flag cleared). If sfsMount() can
still find the file and its exact bytes afterward, the filesystem is
genuinely backed by disk, not by a cache that happens to survive. */
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("keep") > 0);
int32 f = sfsCreateFile("keep/data.bin");
KT_TRUE(t, f > 0);
const char* payload = "this must survive a real remount";
uint64 len = strlen(payload);
KT_EQ(t, sfsWriteAt(f, 0, payload, len), (int64)len);
sfsUnmount();
KT_FALSE(t, sfsMounted());
KT_TRUE(t, sfsMount());
KT_TRUE(t, sfsMounted());
int32 reloaded = sfsLookup("keep/data.bin");
KT_EQ(t, reloaded, f);
KT_EQ(t, sfsSizeOf(reloaded), len);
char back[64];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(reloaded, 0, back, len), (int64)len);
KT_EQ(t, memcmp(back, payload, len), 0);
}
KTEST(sfsMountWithoutFormatRecoversPriorState)
{
/* sfsMount() on an already-formatted disk must NOT reformat: a second
mount call has to see the first mount's data, exactly like a real
reboot re-mounting an existing filesystem. */
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("survives.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "still here", 10), 10);
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 found = sfsLookup("survives.txt");
KT_EQ(t, found, f);
char back[10];
KT_EQ(t, sfsReadAt(found, 0, back, 10), 10);
KT_EQ(t, memcmp(back, "still here", 10), 0);
}
/*
* mtime/ctime coverage below. Every bound uses a real rtcNow() reading taken
* strictly before or after the operation under test, in this same thread, so
* "before <= stamped-value <= after" is a real chronological fact (the clock
* only ever moves forward), never a flaky timing assumption -- unlike a
* cross-run "did it increase" check, which would need a real elapsed second
* to be meaningful. sfsLinkBumpsCtimeButNeverTouchesMtime instead checks
* exact, timing-independent equality: the link code path must never write
* inode.mtime at all, so it has to come back byte-for-byte unchanged
* regardless of how much wall-clock time passed.
*/
KTEST(sfsCreateFileStampsMtimeAndCtimeNearNow)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int64 before = rtcNow();
int32 f = sfsCreateFile("stamped.txt");
if (!KT_TRUE(t, f > 0))
return;
int64 after = rtcNow();
uint64 mtime = sfsMtimeOf(f);
uint64 ctime = sfsCtimeOf(f);
KT_EQ(t, mtime, ctime);
KT_GE(t, mtime, before);
KT_LE(t, mtime, after);
}
KTEST(sfsWriteAtBumpsMtimeAndCtimeStayingInSync)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("written.txt");
if (!KT_TRUE(t, f > 0))
return;
uint64 createdMtime = sfsMtimeOf(f);
int64 before = rtcNow();
const char* payload = "sfs mtime test payload";
uint64 len = strlen(payload);
KT_EQ(t, sfsWriteAt(f, 0, payload, len), (int64)len);
int64 after = rtcNow();
uint64 mtime = sfsMtimeOf(f);
uint64 ctime = sfsCtimeOf(f);
KT_EQ(t, mtime, ctime);
KT_GE(t, mtime, createdMtime);
KT_GE(t, mtime, before);
KT_LE(t, mtime, after);
}
KTEST(sfsLinkBumpsCtimeButNeverTouchesMtime)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("original.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQ(t, sfsWriteAt(f, 0, "hi", 2), 2);
uint64 mtimeBeforeLink = sfsMtimeOf(f);
uint64 ctimeBeforeLink = sfsCtimeOf(f);
int64 before = rtcNow();
KT_EQ(t, sfsLink("original.txt", "alias.txt"), f);
int64 after = rtcNow();
KT_EQ(t, sfsMtimeOf(f), mtimeBeforeLink);
KT_GE(t, sfsCtimeOf(f), ctimeBeforeLink);
KT_GE(t, sfsCtimeOf(f), before);
KT_LE(t, sfsCtimeOf(f), after);
}
KTEST(sfsRemoveOneOfTwoHardlinksBumpsRemainingLinkCtime)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("keepme.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQ(t, sfsLink("keepme.txt", "alias2.txt"), f);
KT_EQ(t, sfsLinkCountOf(f), 2u);
uint64 ctimeBeforeRemove = sfsCtimeOf(f);
int64 before = rtcNow();
KT_TRUE(t, sfsRemove("alias2.txt"));
int64 after = rtcNow();
KT_EQ(t, sfsLinkCountOf(f), 1u);
KT_GE(t, sfsCtimeOf(f), ctimeBeforeRemove);
KT_GE(t, sfsCtimeOf(f), before);
KT_LE(t, sfsCtimeOf(f), after);
}
KTEST(sfsPersistsMtimeAndCtimeAcrossUnmountRemount)
{
/* Same real point as sfsPersistsAcrossUnmountRemount above, applied to
the new fields: sfsUnmount() drops every in-memory structure, so an
exact match after sfsMount() proves these timestamps genuinely live on
disk inside the inode, not in some cache that happens to survive. */
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("timepersist.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQ(t, sfsWriteAt(f, 0, "persisted", 9), 9);
uint64 mtimeBefore = sfsMtimeOf(f);
uint64 ctimeBefore = sfsCtimeOf(f);
KT_GT(t, mtimeBefore, 0);
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 reloaded = sfsLookup("timepersist.txt");
KT_EQ(t, reloaded, f);
KT_EQ(t, sfsMtimeOf(reloaded), mtimeBefore);
KT_EQ(t, sfsCtimeOf(reloaded), ctimeBefore);
}
/* Real chmod(2) owner rwx bits (fs.h's fsModeOwner*): createNode stamps a
type-based default (a file gets fsModeDefaultFile, a directory gets
fsModeDefaultDir), sfsChmod overwrites it and bumps ctime like any other
metadata-only mutation (sfsLink's linkCount bump does the same, see
above), and sfsModeOf/sfsChmod on an invalid inode number fail the same
way sfsSizeOf/sfsTypeOf already do for one. */
KTEST(sfsModeDefaultsByTypeAndChmodBumpsCtime)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("modefile.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQ(t, sfsModeOf(f), fsModeDefaultFile);
int32 d = sfsCreateDir("modedir");
if (!KT_TRUE(t, d > 0))
return;
KT_EQ(t, sfsModeOf(d), fsModeDefaultDir);
uint64 ctimeBefore = sfsCtimeOf(f);
int64 before = rtcNow();
KT_TRUE(t, sfsChmod(f, 0400));
int64 after = rtcNow();
KT_EQ(t, sfsModeOf(f), 0400u);
KT_GE(t, sfsCtimeOf(f), ctimeBefore);
KT_GE(t, sfsCtimeOf(f), before);
KT_LE(t, sfsCtimeOf(f), after);
KT_EQ(t, sfsModeOf(-1), 0u);
KT_EQ(t, sfsModeOf(9999), 0u);
KT_FALSE(t, sfsChmod(-1, 0644));
KT_FALSE(t, sfsChmod(9999, 0644));
}
/* Same real point as sfsPersistsMtimeAndCtimeAcrossUnmountRemount above,
applied to mode: sfsUnmount() drops every in-memory structure, so an
exact match after sfsMount() proves the chmod'd value genuinely lives on
disk inside the inode, not in some cache that happens to survive. */
KTEST(sfsPersistsModeAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("modepersist.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_TRUE(t, sfsChmod(f, 0400));
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 reloaded = sfsLookup("modepersist.txt");
KT_EQ(t, reloaded, f);
KT_EQ(t, sfsModeOf(reloaded), 0400u);
}
/* Real owner identity (fs.h's FbStat.uid, sched.h's currentThreadUid()):
createNode stamps every new node -- file, dir, symlink, fifo all go
through it, exactly like mtime/ctime already verified sufficient for
(FALA 39's own note) -- with whoever is CURRENT at creation time.
ktestRunAll always runs as the permanent idle thread (id 0, uid 0), the
same context schedTest.c's own uid-default tests already document
relying on, so both a freshly created file and the root inode itself
(sfsFormat, this test's own sfsFormat() call included) come back
root-owned. sfsUidOf on an invalid inode number fails the same way
sfsModeOf/sfsSizeOf/sfsTypeOf already do for one. */
KTEST(sfsCreateNodeStampsCreatorUidAsOwner)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_EQU(t, sfsUidOf(sfsRootInode), 0u);
int32 f = sfsCreateFile("uidfile.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQU(t, sfsUidOf(f), 0u);
int32 d = sfsCreateDir("uiddir");
if (!KT_TRUE(t, d > 0))
return;
KT_EQU(t, sfsUidOf(d), 0u);
KT_EQU(t, sfsUidOf(-1), 0u);
KT_EQU(t, sfsUidOf(9999), 0u);
}
/* Same real point as sfsPersistsModeAcrossUnmountRemount above, applied to
uid: sfsUnmount() drops every in-memory structure, so an exact match
after sfsMount() proves ownership genuinely lives on disk inside the
inode, not in some cache that happens to survive. */
KTEST(sfsPersistsUidAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("uidpersist.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQU(t, sfsUidOf(f), 0u);
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 reloaded = sfsLookup("uidpersist.txt");
KT_EQ(t, reloaded, f);
KT_EQU(t, sfsUidOf(reloaded), 0u);
}
/* Real chown(2)'s on-disk half (sfsChown): overwrites the inode's owner
uid outright and bumps ctime like sfsChmod's own metadata-only mutation
does -- same "bump ctime, invalid inode fails the same way" shape as
sfsModeDefaultsByTypeAndChmodBumpsCtime above. sfsChown itself has no
privilege check (vfsChown, vfs.c, is the real choke point that enforces
chown(2)'s root-only rule), so calling it directly here at any uid is
deliberately not restricted. */
KTEST(sfsChownReassignsOwnerAndBumpsCtime)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("chownfile.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQU(t, sfsUidOf(f), 0u);
uint64 ctimeBefore = sfsCtimeOf(f);
int64 before = rtcNow();
KT_TRUE(t, sfsChown(f, 9001));
int64 after = rtcNow();
KT_EQU(t, sfsUidOf(f), 9001u);
KT_GE(t, sfsCtimeOf(f), ctimeBefore);
KT_GE(t, sfsCtimeOf(f), before);
KT_LE(t, sfsCtimeOf(f), after);
KT_FALSE(t, sfsChown(-1, 1000));
KT_FALSE(t, sfsChown(9999, 1000));
}
/* Same real point as sfsPersistsUidAcrossUnmountRemount above, but for a
uid REASSIGNED by sfsChown after creation, not just the creation-time
default -- proves the chown write path itself lands on disk, not just
that createNode's own initial stamp does. */
KTEST(sfsPersistsChownedUidAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("chownpersist.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_TRUE(t, sfsChown(f, 8123));
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 reloaded = sfsLookup("chownpersist.txt");
KT_EQ(t, reloaded, f);
KT_EQU(t, sfsUidOf(reloaded), 8123u);
}
/* Real chgrp(2)'s on-disk half (sfsChgrp), mirroring
sfsChownReassignsOwnerAndBumpsCtime exactly one field over -- same "no
privilege check of its own, ctime bumps, invalid inode fails the same
way" shape. */
KTEST(sfsChgrpReassignsGroupAndBumpsCtime)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("chgrpfile.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_EQU(t, sfsGidOf(f), 0u);
uint64 ctimeBefore = sfsCtimeOf(f);
int64 before = rtcNow();
KT_TRUE(t, sfsChgrp(f, 9002));
int64 after = rtcNow();
KT_EQU(t, sfsGidOf(f), 9002u);
KT_GE(t, sfsCtimeOf(f), ctimeBefore);
KT_GE(t, sfsCtimeOf(f), before);
KT_LE(t, sfsCtimeOf(f), after);
KT_FALSE(t, sfsChgrp(-1, 1000));
KT_FALSE(t, sfsChgrp(9999, 1000));
}
/* Same real point as sfsPersistsChownedUidAcrossUnmountRemount above, one
field over: proves the chgrp write path itself lands on disk. */
KTEST(sfsPersistsChgrpedGidAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("chgrppersist.txt");
if (!KT_TRUE(t, f > 0))
return;
KT_TRUE(t, sfsChgrp(f, 7124));
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 reloaded = sfsLookup("chgrppersist.txt");
KT_EQ(t, reloaded, f);
KT_EQU(t, sfsGidOf(reloaded), 7124u);
}
/* Real directory-write enforcement at the on-disk choke point (sfs.c's
createNode, shared by sfsCreateFile/sfsCreateDir/sfsCreateFifo/
sfsCreateSymlink) -- mirrors coreTest.c's tmpfs
coreVfsMkdirAndCreateRequireParentWrite for the SFS backing store.
ktestRunAll always runs as uid 0, and vfsPermitted has no uid-0 bypass,
so removing the parent's own owner-write bit denies even its own
creator/owner. */
KTEST(sfsCreateNodeRequiresParentWrite)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("dwparent") > 0);
KT_TRUE(t, sfsChmod(sfsLookup("dwparent"), 0555)); /* r-x, no owner-write */
KT_EQ(t, sfsCreateFile("dwparent/f"), -1);
KT_EQ(t, sfsCreateDir("dwparent/sub"), -1);
KT_TRUE(t, sfsChmod(sfsLookup("dwparent"), fsModeDefaultDir));
KT_TRUE(t, sfsCreateFile("dwparent/f") > 0);
KT_TRUE(t, sfsCreateDir("dwparent/sub") > 0);
}
/* Same parent-write rule applied to sfsLink -- adding a new hardlink NAME
is a directory-entry creation exactly like sfsCreateFile, so it needs
write permission on the PARENT of the new name, not any permission on
the existing target being linked to. */
KTEST(sfsLinkRequiresParentWriteOnNewName)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateFile("lwtarget.txt") > 0);
KT_TRUE(t, sfsCreateDir("lwparent") > 0);
KT_TRUE(t, sfsChmod(sfsLookup("lwparent"), 0555));
KT_EQ(t, sfsLink("lwtarget.txt", "lwparent/alias"), -1);
KT_TRUE(t, sfsChmod(sfsLookup("lwparent"), fsModeDefaultDir));
KT_TRUE(t, sfsLink("lwtarget.txt", "lwparent/alias") > 0);
}
/* Real unlink(2)/rmdir(2) semantics at the on-disk choke point (sfs.c's
sfsRemove) -- mirrors coreTest.c's tmpfs
coreVfsUnlinkAndRmdirRequireParentWrite: removing an entry needs write
permission on the PARENT, never the entry's own mode. */
KTEST(sfsRemoveRequiresParentWrite)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("rwparent") > 0);
KT_TRUE(t, sfsCreateFile("rwparent/f") > 0);
KT_TRUE(t, sfsChmod(sfsLookup("rwparent"), 0555));
KT_FALSE(t, sfsRemove("rwparent/f"));
KT_TRUE(t, sfsChmod(sfsLookup("rwparent"), fsModeDefaultDir));
KT_TRUE(t, sfsRemove("rwparent/f"));
}
/* Real POSIX search permission at the on-disk choke point (sfs.c's
lookupFrom, checked on every directory ANCESTOR a path walks through) --
mirrors coreTest.c's tmpfs coreVfsLookupRequiresAncestorExec. Removing
the exec bit on an ancestor makes every lookup THROUGH it fail even
though the target inode itself never changed. */
KTEST(sfsLookupRequiresAncestorExec)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("xancestor") > 0);
int32 leaf = sfsCreateFile("xancestor/leaf");
KT_TRUE(t, leaf > 0);
KT_TRUE(t, sfsChmod(sfsLookup("xancestor"), 0600)); /* rw-, no exec */
KT_EQ(t, sfsLookup("xancestor/leaf"), -1);
KT_TRUE(t, sfsChmod(sfsLookup("xancestor"), fsModeDefaultDir));
KT_EQ(t, sfsLookup("xancestor/leaf"), leaf);
}
KTEST(sfsSymlinkResolvesToTargetFile)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
const char* msg = "reached through a link";
uint64 len = strlen(msg);
KT_EQ(t, sfsWriteAt(f, 0, msg, len), (int64)len);
int32 link = sfsCreateSymlink("link.txt", "real.txt");
KT_TRUE(t, link > 0);
KT_NE(t, link, f);
/* sfsLookup follows the symlink: it must land on the SAME inode as
looking the real file up directly, not a copy of it. */
KT_EQ(t, sfsLookup("link.txt"), f);
KT_EQ(t, sfsTypeOf(sfsLookup("link.txt")), sfsTypeFile);
char back[64];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(sfsLookup("link.txt"), 0, back, len), (int64)len);
KT_EQ(t, memcmp(back, msg, len), 0);
}
KTEST(sfsReadlinkReturnsStoredTargetAndNoFollowSeesTheLinkItself)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
int32 link = sfsCreateSymlink("link.txt", "real.txt");
KT_TRUE(t, link > 0);
char target[32];
memset(target, 0, sizeof(target));
KT_TRUE(t, sfsReadlink(link, target, sizeof(target)));
KT_STREQ(t, target, "real.txt");
/* No-follow gives back the symlink's own inode (and its own type),
distinct from what sfsLookup (which follows) would give. */
int32 noFollow = sfsLookupNoFollow("link.txt");
KT_EQ(t, noFollow, link);
KT_EQ(t, sfsTypeOf(noFollow), sfsTypeSymlink);
KT_NE(t, noFollow, f);
}
KTEST(sfsReadlinkRejectsNonSymlinkAndUndersizedBuffer)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("plain.txt");
KT_TRUE(t, f > 0);
char buf[8];
KT_FALSE(t, sfsReadlink(f, buf, sizeof(buf)));
int32 link = sfsCreateSymlink("link.txt", "a target long enough to not fit");
KT_TRUE(t, link > 0);
char tiny[4];
KT_FALSE(t, sfsReadlink(link, tiny, sizeof(tiny)));
}
KTEST(sfsSymlinkToDirectoryAllowsDescendingThroughIt)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("realdir") > 0);
int32 f = sfsCreateFile("realdir/inside.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "found me", 8), 8);
int32 link = sfsCreateSymlink("linkdir", "realdir");
KT_TRUE(t, link > 0);
KT_EQ(t, sfsTypeOf(link), sfsTypeSymlink);
/* The symlink is an INTERMEDIATE component here (there is more path
after it), which must still be followed even though sfsLookup's
"final component" special case does not apply. */
int32 viaLink = sfsLookup("linkdir/inside.txt");
KT_EQ(t, viaLink, f);
/* Creating a new file through the symlinked directory must land in the
real directory, reachable from both paths. */
KT_TRUE(t, sfsCreateFile("linkdir/new.txt") > 0);
KT_EQ(t, sfsLookup("realdir/new.txt"), sfsLookup("linkdir/new.txt"));
}
KTEST(sfsDanglingSymlinkFailsLookupButReadlinkStillWorks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 link = sfsCreateSymlink("broken.txt", "does/not/exist.txt");
KT_TRUE(t, link > 0);
KT_EQ(t, sfsLookup("broken.txt"), -1);
char target[64];
memset(target, 0, sizeof(target));
KT_TRUE(t, sfsReadlink(link, target, sizeof(target)));
KT_STREQ(t, target, "does/not/exist.txt");
}
KTEST(sfsSymlinkCycleFailsCleanlyInsteadOfHanging)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateSymlink("a", "b") > 0);
KT_TRUE(t, sfsCreateSymlink("b", "a") > 0);
/* The whole point: this must return with an error, not recurse forever
(or overflow the caller's stack) chasing a->b->a->b->... */
KT_EQ(t, sfsLookup("a"), -1);
KT_EQ(t, sfsLookup("b"), -1);
/* The table must still be in a usable state afterward - a failed
resolution is not the same thing as a corrupted filesystem. */
KT_TRUE(t, sfsCreateFile("stillworks.txt") > 0);
}
KTEST(sfsCreateSymlinkRejectsEmptyOrOversizedTarget)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_EQ(t, sfsCreateSymlink("empty.txt", ""), -1);
char huge[sfsSymlinkTargetMax + 2];
memset(huge, 'x', sizeof(huge) - 1);
huge[sizeof(huge) - 1] = 0;
KT_EQ(t, sfsCreateSymlink("huge.txt", huge), -1);
/* Rejected attempts must not leave a half-created inode or directory
entry behind. */
KT_EQ(t, sfsLookupNoFollow("empty.txt"), -1);
KT_EQ(t, sfsLookupNoFollow("huge.txt"), -1);
}
KTEST(sfsRemoveSymlinkDoesNotAffectTarget)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "untouched", 9), 9);
KT_TRUE(t, sfsCreateSymlink("link.txt", "real.txt") > 0);
KT_TRUE(t, sfsRemove("link.txt"));
KT_EQ(t, sfsLookupNoFollow("link.txt"), -1);
/* The target must survive with its data intact - unlink of a symlink
must never recurse into what it points at. */
int32 stillThere = sfsLookup("real.txt");
KT_EQ(t, stillThere, f);
char back[16];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(stillThere, 0, back, 9), 9);
KT_EQ(t, memcmp(back, "untouched", 9), 0);
}
KTEST(sfsSymlinkPersistsAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "on disk for real", 17), 17);
int32 link = sfsCreateSymlink("link.txt", "real.txt");
KT_TRUE(t, link > 0);
sfsUnmount();
KT_TRUE(t, sfsMount());
KT_EQ(t, sfsLookup("link.txt"), f);
KT_EQ(t, sfsLookupNoFollow("link.txt"), link);
KT_EQ(t, sfsTypeOf(link), sfsTypeSymlink);
char target[32];
memset(target, 0, sizeof(target));
KT_TRUE(t, sfsReadlink(link, target, sizeof(target)));
KT_STREQ(t, target, "real.txt");
}
KTEST(sfsFileGrowsThroughDoubleIndirectBlock)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("huge.bin");
KT_TRUE(t, f > 0);
/* sfsDirectBlocks (10) + sfsPtrsPerBlock (128) = 138 logical blocks are
reachable through direct + single indirect addressing; block 138
onward can only be reached through the double indirect block added
in this wave. Starting a few blocks before that boundary and writing
past it exercises the crossing and the double-indirect-only region
in one write (sfsWriteAt zero-fills 0..134 first, all still within
single-indirect range, before this pattern lands on 135..144). */
#define sfsDoubleSpanBlocks 10
#define sfsDoubleStartBlock 135
#define sfsDoubleSpan (sfsDoubleSpanBlocks * sfsBlockSize)
uint8* pattern = (uint8*)kmalloc(sfsDoubleSpan);
uint8* verify = (uint8*)kmalloc(sfsDoubleSpan);
KT_NOTNULL(t, pattern);
KT_NOTNULL(t, verify);
if (!pattern || !verify) {
kfree(pattern);
kfree(verify);
return;
}
for (int i = 0; i < sfsDoubleSpan; i++)
pattern[i] = (uint8)(i * 17 + 3);
uint64 offset = (uint64)sfsDoubleStartBlock * sfsBlockSize;
KT_EQ(t, sfsWriteAt(f, offset, pattern, sfsDoubleSpan), sfsDoubleSpan);
KT_EQ(t, sfsSizeOf(f), offset + sfsDoubleSpan);
memset(verify, 0, sfsDoubleSpan);
KT_EQ(t, sfsReadAt(f, offset, verify, sfsDoubleSpan), sfsDoubleSpan);
KT_EQ(t, memcmp(verify, pattern, sfsDoubleSpan), 0);
/* Patch a window straddling the single-indirect/double-indirect
boundary itself (logical block 137/138) and confirm only that slice
changed -- the same "boundary patch" shape as
sfsFileGrowsThroughIndirectBlock, one level deeper. */
uint8 patch[48];
memset(patch, 0xCC, sizeof(patch));
uint64 patchOffset = 138 * sfsBlockSize - 24;
KT_EQ(t, sfsWriteAt(f, patchOffset, patch, sizeof(patch)), (int64)sizeof(patch));
KT_EQ(t, sfsReadAt(f, offset, verify, sfsDoubleSpan), sfsDoubleSpan);
uint64 patchInSpan = patchOffset - offset;
KT_EQ(t, memcmp(verify + patchInSpan, patch, sizeof(patch)), 0);
KT_EQ(t, memcmp(verify, pattern, patchInSpan), 0);
kfree(pattern);
kfree(verify);
#undef sfsDoubleSpan
#undef sfsDoubleStartBlock
#undef sfsDoubleSpanBlocks
}
KTEST(sfsTruncateFreesDoubleIndirectBlocks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("hugeshrink.bin");
KT_TRUE(t, f > 0);
/* Baseline is taken AFTER creating the file, not before: creating the
very first entry in a previously-empty root directory grows the
root's own data block permanently (it is a directory entry, not file
content, and stays even after truncating the FILE back to empty) --
that is correct, unrelated bookkeeping this test must not conflate
with whether truncating hugeshrink.bin's own content gives its
blocks back. */
uint64 baseline = sfsFreeDataBlocks();
KT_GT(t, baseline, 0);
#define sfsShrinkSpanBlocks 8
#define sfsShrinkStartBlock 136
#define sfsShrinkSpan (sfsShrinkSpanBlocks * sfsBlockSize)
uint8* chunk = (uint8*)kmalloc(sfsShrinkSpan);
KT_NOTNULL(t, chunk);
if (!chunk)
return;
memset(chunk, 0x91, sfsShrinkSpan);
uint64 offset = (uint64)sfsShrinkStartBlock * sfsBlockSize;
KT_EQ(t, sfsWriteAt(f, offset, chunk, sfsShrinkSpan), sfsShrinkSpan);
kfree(chunk);
#undef sfsShrinkSpan
#undef sfsShrinkStartBlock
#undef sfsShrinkSpanBlocks
/* This file now owns: 10 direct + 1 single-indirect holder + 128
single-indirect data blocks + 1 double-indirect holder + 1 inner
indirect holder + a handful of double-indirect data blocks. Every
one of those must come back. */
KT_LT(t, sfsFreeDataBlocks(), baseline);
KT_TRUE(t, sfsTruncate(f, 0));
KT_EQ(t, sfsSizeOf(f), 0);
KT_EQU(t, sfsFreeDataBlocks(), baseline);
/* And the inode itself must still be usable afterward, not left in some
half-truncated state. */
KT_EQ(t, sfsWriteAt(f, 0, "reused", 6), 6);
KT_EQ(t, sfsSizeOf(f), 6);
}
KTEST(sfsFileGrowsThroughTripleIndirectBlock)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("huger.bin");
KT_TRUE(t, f > 0);
/* sfsDirectBlocks (10) + sfsPtrsPerBlock (128) + sfsPtrsPerBlock^2
(16384) = 16522 logical blocks are reachable through direct + single
+ double indirect addressing; block 16522 onward can only be reached
through the triple indirect block added in this wave. Physically
reaching that boundary with real writes from offset 0 would mean
sfsWriteAt zero-filling ~8 MiB of real leading blocks first (its own
"no sparse holes" contract, see its doc comment) -- correct, but far
too slow for a headless boot-time gate. sfsTruncate's own grow path
is a genuine sparse hole instead (see sfsTruncateInode: growing only
ever rewrites the size field, no block is touched), the same
lseek-past-EOF-then-write shape a real filesystem's holes have -- so
pre-growing with it reaches the boundary for free, in O(1), then a
real write of a handful of blocks exercises the actual double/triple
crossing without paying for the untouched blocks in between. */
#define sfsTripleBoundaryBlock 16522
#define sfsTripleSpanBlocks 12
#define sfsTripleStartBlock (sfsTripleBoundaryBlock - 6)
#define sfsTripleSpan (sfsTripleSpanBlocks * sfsBlockSize)
uint64 startOffset = (uint64)sfsTripleStartBlock * sfsBlockSize;
KT_TRUE(t, sfsTruncate(f, startOffset));
KT_EQ(t, sfsSizeOf(f), startOffset);
uint8* pattern = (uint8*)kmalloc(sfsTripleSpan);
uint8* verify = (uint8*)kmalloc(sfsTripleSpan);
KT_NOTNULL(t, pattern);
KT_NOTNULL(t, verify);
if (!pattern || !verify) {
kfree(pattern);
kfree(verify);
return;
}
for (int i = 0; i < sfsTripleSpan; i++)
pattern[i] = (uint8)(i * 29 + 7);
/* Six blocks land in the double-indirect region (16516..16521), the
other six only exist through the new triple-indirect block
(16522..16527) -- one write straddles both. */
KT_EQ(t, sfsWriteAt(f, startOffset, pattern, sfsTripleSpan), sfsTripleSpan);
KT_EQ(t, sfsSizeOf(f), startOffset + sfsTripleSpan);
memset(verify, 0, sfsTripleSpan);
KT_EQ(t, sfsReadAt(f, startOffset, verify, sfsTripleSpan), sfsTripleSpan);
KT_EQ(t, memcmp(verify, pattern, sfsTripleSpan), 0);
/* Patch a window straddling the double-indirect/triple-indirect
boundary itself (logical block 16521/16522) -- same "boundary patch"
shape as sfsFileGrowsThroughDoubleIndirectBlock, one level deeper. */
uint8 patch[48];
memset(patch, 0xAB, sizeof(patch));
uint64 patchOffset = (uint64)sfsTripleBoundaryBlock * sfsBlockSize - 24;
KT_EQ(t, sfsWriteAt(f, patchOffset, patch, sizeof(patch)), (int64)sizeof(patch));
KT_EQ(t, sfsReadAt(f, startOffset, verify, sfsTripleSpan), sfsTripleSpan);
uint64 patchInSpan = patchOffset - startOffset;
KT_EQ(t, memcmp(verify + patchInSpan, patch, sizeof(patch)), 0);
KT_EQ(t, memcmp(verify, pattern, patchInSpan), 0);
kfree(pattern);
kfree(verify);
#undef sfsTripleSpan
#undef sfsTripleStartBlock
#undef sfsTripleSpanBlocks
#undef sfsTripleBoundaryBlock
}
KTEST(sfsTruncateFreesTripleIndirectBlocks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("hugershrink.bin");
KT_TRUE(t, f > 0);
/* Baseline taken after creating the file, same reasoning as
sfsTruncateFreesDoubleIndirectBlocks: the root directory's own data
block is unrelated bookkeeping this test must not conflate with
hugershrink.bin's own content. */
uint64 baseline = sfsFreeDataBlocks();
KT_GT(t, baseline, 0);
/* Grow straight to the triple-indirect boundary via sfsTruncate (O(1),
a genuine sparse hole -- see sfsFileGrowsThroughTripleIndirectBlock
above) and write ENTIRELY inside the triple-indirect region, never
touching a single/double-indirect block. That keeps inode->indirect
and inode->doubleIndirect at 0, so the shrink-to-0 below only ever
does a cheap in-memory "still unallocated" check across the whole
single+double logical range instead of re-reading real holder blocks
for it -- proportional to the actual work, not to how far into the
address space the file happens to reach. */
#define sfsTripleBoundaryBlock 16522
#define sfsShrink2SpanBlocks 8
#define sfsShrink2Span (sfsShrink2SpanBlocks * sfsBlockSize)
uint64 offset = (uint64)sfsTripleBoundaryBlock * sfsBlockSize;
KT_TRUE(t, sfsTruncate(f, offset));
uint8* chunk = (uint8*)kmalloc(sfsShrink2Span);
KT_NOTNULL(t, chunk);
if (!chunk)
return;
memset(chunk, 0x62, sfsShrink2Span);
KT_EQ(t, sfsWriteAt(f, offset, chunk, sfsShrink2Span), sfsShrink2Span);
kfree(chunk);
#undef sfsShrink2Span
#undef sfsShrink2SpanBlocks
#undef sfsTripleBoundaryBlock
/* This file now owns: 1 triple-indirect holder + 1 mid holder + 1
leaf-pointer holder + a handful of triple-indirect data blocks.
Every one of those must come back. */
KT_LT(t, sfsFreeDataBlocks(), baseline);
KT_TRUE(t, sfsTruncate(f, 0));
KT_EQ(t, sfsSizeOf(f), 0);
KT_EQU(t, sfsFreeDataBlocks(), baseline);
/* And the inode itself must still be usable afterward. */
KT_EQ(t, sfsWriteAt(f, 0, "reused", 6), 6);
KT_EQ(t, sfsSizeOf(f), 6);
}
KTEST(sfsHardlinkSharesInodeAndData)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("original.txt");
KT_TRUE(t, f > 0);
const char* msg = "shared bytes";
uint64 len = strlen(msg);
KT_EQ(t, sfsWriteAt(f, 0, msg, len), (int64)len);
KT_EQ(t, sfsLinkCountOf(f), 1);
int32 linked = sfsLink("original.txt", "alias.txt");
KT_EQ(t, linked, f);
KT_EQ(t, sfsLinkCountOf(f), 2);
KT_EQ(t, sfsLookup("alias.txt"), f);
/* A write through the alias must be visible through the original name:
they are not two files that happen to match, they are the same
inode. */
KT_EQ(t, sfsWriteAt(sfsLookup("alias.txt"), len, " too", 4), 4);
char back[32];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(f, 0, back, len + 4), (int64)(len + 4));
KT_EQ(t, memcmp(back, "shared bytes too", len + 4), 0);
}
KTEST(sfsHardlinkRejectsDirectoriesAndBadArguments)
{
if (!KT_TRUE(t, sfsFormat()))
return;
KT_TRUE(t, sfsCreateDir("adir") > 0);
KT_EQ(t, sfsLink("adir", "adiralias"), -1);
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsLink("does/not/exist.txt", "alias.txt"), -1);
KT_TRUE(t, sfsCreateFile("taken.txt") > 0);
KT_EQ(t, sfsLink("real.txt", "taken.txt"), -1);
/* None of the rejected attempts may have left a stray link count or
directory entry behind. */
KT_EQ(t, sfsLinkCountOf(f), 1);
KT_EQ(t, sfsLookup("alias.txt"), -1);
}
KTEST(sfsRemoveOneHardlinkKeepsOtherAlive)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("first.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "durable", 7), 7);
KT_TRUE(t, sfsLink("first.txt", "second.txt") > 0);
KT_TRUE(t, sfsRemove("first.txt"));
KT_EQ(t, sfsLookup("first.txt"), -1);
int32 viaSecond = sfsLookup("second.txt");
KT_EQ(t, viaSecond, f);
KT_EQ(t, sfsLinkCountOf(viaSecond), 1);
char back[8];
KT_EQ(t, sfsReadAt(viaSecond, 0, back, 7), 7);
KT_EQ(t, memcmp(back, "durable", 7), 0);
}
KTEST(sfsRemoveLastHardlinkFreesInodeAndBlocks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
uint64 baseline = sfsFreeDataBlocks();
int32 f = sfsCreateFile("only.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "temporary", 9), 9);
KT_TRUE(t, sfsLink("only.txt", "also.txt") > 0);
KT_LT(t, sfsFreeDataBlocks(), baseline);
KT_TRUE(t, sfsRemove("only.txt"));
KT_NE(t, sfsLookup("also.txt"), -1); /* one link left: data still lives */
KT_TRUE(t, sfsRemove("also.txt"));
KT_EQ(t, sfsLookup("also.txt"), -1);
KT_EQU(t, sfsFreeDataBlocks(), baseline);
/* The inode slot must be reusable, not permanently burned. */
int32 again = sfsCreateFile("only.txt");
KT_TRUE(t, again > 0);
KT_EQ(t, sfsSizeOf(again), 0);
}
KTEST(sfsHardlinkThroughSymlinkTargetsRealInode)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
KT_TRUE(t, sfsCreateSymlink("link.txt", "real.txt") > 0);
/* Linking through a symlink must attach the new name to whatever the
symlink resolves to, not to the symlink inode itself (matches
link()'s default follow-existingPath semantics, documented on
sfsLink). */
int32 linked = sfsLink("link.txt", "alias.txt");
KT_EQ(t, linked, f);
KT_EQ(t, sfsLinkCountOf(f), 2);
KT_EQ(t, sfsTypeOf(sfsLookup("alias.txt")), sfsTypeFile);
}
KTEST(sfsHardlinkPersistsAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("orig.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "persisted", 9), 9);
KT_TRUE(t, sfsLink("orig.txt", "alias.txt") > 0);
sfsUnmount();
KT_TRUE(t, sfsMount());
int32 viaOrig = sfsLookup("orig.txt");
int32 viaAlias = sfsLookup("alias.txt");
KT_EQ(t, viaOrig, f);
KT_EQ(t, viaOrig, viaAlias);
KT_EQ(t, sfsLinkCountOf(f), 2);
char back[16];
memset(back, 0, sizeof(back));
KT_EQ(t, sfsReadAt(viaAlias, 0, back, 9), 9);
KT_EQ(t, memcmp(back, "persisted", 9), 0);
}
/* Leave the filesystem in a clean, empty, formatted state for any code that
runs after the ktest battery (e.g. a userspace program exercising /mnt
through the real syscall path). */
KTEST(sfsFormatLeavesCleanStateForLaterUse)
{
KT_TRUE(t, sfsFormat());
KT_EQ(t, sfsLookup(""), sfsRootInode);
char name[sfsNameMax + 1];
uint32 type;
KT_FALSE(t, sfsReaddirAt(sfsRootInode, 0, name, sizeof(name), &type));
}