diff --git a/user/linktest.c b/user/linktest.c new file mode 100644 index 0000000..b6f4b25 --- /dev/null +++ b/user/linktest.c @@ -0,0 +1,156 @@ +#include "ulib.h" +#include "fs.h" + +/* + * Exercises the symlink/readlink/link syscalls (sysSymlink/sysReadlink/ + * sysLink -> vfs.c -> sfs.c, sfs.h already had the on-disk machinery from an + * earlier wave; this wave wires it up to ring 3) against the persistent SFS + * filesystem under "/mnt". Kernel-side symlink target strings are resolved + * relative to the SFS root (sfs.h), not to "/mnt" -- so a symlink created + * from userspace at "/mnt/linktest/link.txt" pointing at + * "/mnt/linktest/real.txt" must store the target as "/linktest/real.txt" + * (the "/mnt" prefix stripped, exactly what vfsSymlink hands to + * sfsCreateSymlink) for later resolution to actually find it. + */ + +static int bytesEqual(const char* a, const char* b, unsigned long n) +{ + for (unsigned long i = 0; i < n; i++) + if (a[i] != b[i]) + return 0; + return 1; +} + +int main(void) +{ + print("linktest: start, pid="); + printDec(userGetpid()); + print("\n"); + + if (userMkdir("/mnt/linktest") != 0) { + print("linktest: FAIL mkdir /mnt/linktest\n"); + return 1; + } + + const char* payload = "frostbyte linktest payload\n"; + unsigned long payloadLen = stringLength(payload); + + int wfd = (int)userOpen("/mnt/linktest/real.txt", oReadWrite | oCreate); + if (wfd < 0) { + print("linktest: FAIL create real.txt\n"); + return 1; + } + if (userWrite(wfd, payload, payloadLen) != (long)payloadLen) { + print("linktest: FAIL write real.txt\n"); + return 1; + } + userClose(wfd); + + /* Symlink resolution through open()/read(): opening the link must + transparently reach real.txt's data. */ + if (userSymlink("/linktest/real.txt", "/mnt/linktest/link.txt") != 0) { + print("linktest: FAIL symlink create\n"); + return 1; + } + + int lfd = (int)userOpen("/mnt/linktest/link.txt", oReadOnly); + if (lfd < 0) { + print("linktest: FAIL open through symlink\n"); + return 1; + } + char back[64]; + long got = userRead(lfd, back, sizeof(back)); + userClose(lfd); + if (got != (long)payloadLen || !bytesEqual(back, payload, payloadLen)) { + print("linktest: FAIL read through symlink mismatch\n"); + return 1; + } + print("linktest: open/read transparently follows symlink ok\n"); + + /* stat() must also follow: size should match the target, not 0. */ + FbStat st; + if (userStat("/mnt/linktest/link.txt", &st) != 0 || st.size != payloadLen) { + print("linktest: FAIL stat through symlink\n"); + return 1; + } + print("linktest: stat follows symlink ok\n"); + + /* readlink() must NOT follow -- it reports the link's own target + string, not the target's contents. */ + char targetBuf[64]; + long targetLen = userReadlink("/mnt/linktest/link.txt", targetBuf, sizeof(targetBuf)); + const char* expectedTarget = "/linktest/real.txt"; + unsigned long expectedTargetLen = stringLength(expectedTarget); + if (targetLen != (long)expectedTargetLen || + !bytesEqual(targetBuf, expectedTarget, expectedTargetLen)) { + print("linktest: FAIL readlink target mismatch\n"); + return 1; + } + print("linktest: readlink reports target string ok\n"); + + /* readlink on a non-symlink must fail, not read garbage. */ + if (userReadlink("/mnt/linktest/real.txt", targetBuf, sizeof(targetBuf)) >= 0) { + print("linktest: FAIL readlink on non-symlink should fail\n"); + return 1; + } + print("linktest: readlink rejects non-symlink ok\n"); + + /* Hardlink: alias.txt shares real.txt's inode and data. */ + if (userLink("/mnt/linktest/real.txt", "/mnt/linktest/alias.txt") != 0) { + print("linktest: FAIL hardlink create\n"); + return 1; + } + int afd = (int)userOpen("/mnt/linktest/alias.txt", oReadOnly); + if (afd < 0) { + print("linktest: FAIL open alias.txt\n"); + return 1; + } + got = userRead(afd, back, sizeof(back)); + userClose(afd); + if (got != (long)payloadLen || !bytesEqual(back, payload, payloadLen)) { + print("linktest: FAIL alias.txt content mismatch\n"); + return 1; + } + print("linktest: hardlink shares data ok\n"); + + /* Directories cannot be hardlinked. */ + if (userLink("/mnt/linktest", "/mnt/linktest2") == 0) { + print("linktest: FAIL hardlinking a directory should be rejected\n"); + return 1; + } + print("linktest: hardlink rejects directory ok\n"); + + /* Removing the original must leave the hardlinked alias alive (link + count keeps the inode+data around). */ + if (userUnlink("/mnt/linktest/real.txt") != 0) { + print("linktest: FAIL unlink real.txt\n"); + return 1; + } + afd = (int)userOpen("/mnt/linktest/alias.txt", oReadOnly); + if (afd < 0) { + print("linktest: FAIL alias.txt gone after unlinking real.txt\n"); + return 1; + } + got = userRead(afd, back, sizeof(back)); + userClose(afd); + if (got != (long)payloadLen || !bytesEqual(back, payload, payloadLen)) { + print("linktest: FAIL alias.txt data lost after unlinking real.txt\n"); + return 1; + } + print("linktest: alias survives unlink of original ok\n"); + + /* Dangling symlink: creation succeeds (target is never validated), open + through it must fail cleanly instead of crashing. */ + if (userSymlink("/linktest/does-not-exist.txt", "/mnt/linktest/dangling.txt") != 0) { + print("linktest: FAIL dangling symlink create\n"); + return 1; + } + if (userOpen("/mnt/linktest/dangling.txt", oReadOnly) >= 0) { + print("linktest: FAIL open through dangling symlink should fail\n"); + return 1; + } + print("linktest: dangling symlink open fails cleanly ok\n"); + + print("linktest: done\n"); + return 0; +}