diff --git a/src/sfsTest.c b/src/sfsTest.c index 63ce0af..8ebe567 100644 --- a/src/sfsTest.c +++ b/src/sfsTest.c @@ -781,3 +781,165 @@ 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); +} +