From 1453540bae221911fdc5d68efb3f183a9ff34c78 Mon Sep 17 00:00:00 2001
From: Sam Tebbs <samuel.tebbs@gmail.com>
Date: Mon, 26 Oct 2020 17:33:20 +0000
Subject: [PATCH] Add symlink support

---
 src/kernel/filesystem/initrd.zig |  19 ++-
 src/kernel/filesystem/vfs.zig    | 210 ++++++++++++++++++++++++++-----
 2 files changed, 190 insertions(+), 39 deletions(-)

diff --git a/src/kernel/filesystem/initrd.zig b/src/kernel/filesystem/initrd.zig
index 8b4e112..aeb215d 100644
--- a/src/kernel/filesystem/initrd.zig
+++ b/src/kernel/filesystem/initrd.zig
@@ -90,10 +90,10 @@ pub const InitrdFS = struct {
     }
 
     /// See vfs.FileSystem.open
-    fn open(fs: *const vfs.FileSystem, dir: *const vfs.DirNode, name: []const u8, flags: vfs.OpenFlags) (Allocator.Error || vfs.Error)!*vfs.Node {
+    fn open(fs: *const vfs.FileSystem, dir: *const vfs.DirNode, name: []const u8, flags: vfs.OpenFlags, args: vfs.OpenArgs) (Allocator.Error || vfs.Error)!*vfs.Node {
         var self = @fieldParentPtr(InitrdFS, "instance", fs.instance);
         switch (flags) {
-            .CREATE_DIR, .CREATE_FILE => return vfs.Error.InvalidFlags,
+            .CREATE_DIR, .CREATE_FILE, .CREATE_SYMLINK => return vfs.Error.InvalidFlags,
             .NO_CREATION => {
                 for (self.files) |*file| {
                     if (std.mem.eql(u8, file.name, name)) {
@@ -380,7 +380,7 @@ test "open valid file" {
     expectEqual(fs.opened_files.count(), 1);
     expectEqualSlices(u8, fs.opened_files.get(file1_node).?.name, "test1.txt");
 
-    var file3_node = try vfs.open("/test3.txt", .NO_CREATION);
+    var file3_node = try vfs.open("/test3.txt", true, .NO_CREATION, .{});
     defer file3_node.File.close();
 
     expectEqual(fs.opened_files.count(), 2);
@@ -388,7 +388,7 @@ test "open valid file" {
 
     var dir1 = try vfs.openDir("/", .NO_CREATION);
     expectEqual(&fs.root_node.Dir, dir1);
-    var file2 = &(try dir1.open("test2.txt", .NO_CREATION)).File;
+    var file2 = &(try dir1.open("test2.txt", .NO_CREATION, .{})).File;
     defer file2.close();
 
     expectEqual(fs.opened_files.count(), 3);
@@ -406,12 +406,19 @@ test "open fail with invalid flags" {
 
     expectError(error.InvalidFlags, vfs.openFile("/text10.txt", .CREATE_DIR));
     expectError(error.InvalidFlags, vfs.openFile("/text10.txt", .CREATE_FILE));
+    expectError(error.InvalidFlags, vfs.openFile("/text10.txt", .CREATE_SYMLINK));
     expectError(error.InvalidFlags, vfs.openDir("/text10.txt", .CREATE_DIR));
     expectError(error.InvalidFlags, vfs.openDir("/text10.txt", .CREATE_FILE));
+    expectError(error.InvalidFlags, vfs.openDir("/text10.txt", .CREATE_SYMLINK));
     expectError(error.InvalidFlags, vfs.openFile("/test/", .CREATE_DIR));
     expectError(error.InvalidFlags, vfs.openFile("/test/", .CREATE_FILE));
+    expectError(error.InvalidFlags, vfs.openFile("/test/", .CREATE_SYMLINK));
     expectError(error.InvalidFlags, vfs.openDir("/test/", .CREATE_DIR));
     expectError(error.InvalidFlags, vfs.openDir("/test/", .CREATE_FILE));
+    expectError(error.InvalidFlags, vfs.openDir("/test/", .CREATE_SYMLINK));
+    expectError(error.InvalidFlags, vfs.openSymlink("/test/", "", .CREATE_FILE));
+    expectError(error.InvalidFlags, vfs.openSymlink("/test/", "", .CREATE_DIR));
+    expectError(error.InvalidFlags, vfs.openSymlink("/test/", "", .CREATE_SYMLINK));
 }
 
 test "open fail with NoSuchFileOrDir" {
@@ -484,7 +491,7 @@ test "close a file" {
 
     expectEqual(fs.opened_files.count(), 1);
 
-    var file3_node = try vfs.open("/test3.txt", .NO_CREATION);
+    var file3_node = try vfs.open("/test3.txt", true, .NO_CREATION, .{});
 
     expectEqual(fs.opened_files.count(), 2);
     file1.close();
@@ -492,7 +499,7 @@ test "close a file" {
 
     var dir1 = try vfs.openDir("/", .NO_CREATION);
     expectEqual(&fs.root_node.Dir, dir1);
-    var file2 = &(try dir1.open("test2.txt", .NO_CREATION)).File;
+    var file2 = &(try dir1.open("test2.txt", .NO_CREATION, .{})).File;
     defer file2.close();
 
     expectEqual(fs.opened_files.count(), 2);
diff --git a/src/kernel/filesystem/vfs.zig b/src/kernel/filesystem/vfs.zig
index 2cd08fe..f4cafc1 100644
--- a/src/kernel/filesystem/vfs.zig
+++ b/src/kernel/filesystem/vfs.zig
@@ -10,16 +10,27 @@ pub const OpenFlags = enum {
     CREATE_DIR,
     /// Create a file if it doesn't exist
     CREATE_FILE,
+    /// Create a symlink if it doesn't exist
+    CREATE_SYMLINK,
     /// Do not create a file or directory
     NO_CREATION,
 };
 
+/// The args used when opening new or existing fs nodes
+pub const OpenArgs = struct {
+    /// What to set the target to when creating a symlink
+    symlink_target: ?[]const u8 = null,
+};
+
 /// A filesystem node that could either be a directory or a file
 pub const Node = union(enum) {
     /// The file node if this represents a file
     File: FileNode,
     /// The dir node if this represents a directory
     Dir: DirNode,
+    /// The absolute path that this symlink is linked to
+    Symlink: []const u8,
+
     const Self = @This();
 
     ///
@@ -34,7 +45,7 @@ pub const Node = union(enum) {
     pub fn isDir(self: Self) bool {
         return switch (self) {
             .Dir => true,
-            .File => false,
+            else => false,
         };
     }
 
@@ -50,7 +61,23 @@ pub const Node = union(enum) {
     pub fn isFile(self: Self) bool {
         return switch (self) {
             .File => true,
-            .Dir => false,
+            else => false,
+        };
+    }
+
+    ///
+    /// Check if this node is a symlink
+    ///
+    /// Arguments:
+    ///     IN self: Self - The node being checked
+    ///
+    /// Return: bool
+    ///     True if this is a symlink else false
+    ///
+    pub fn isSymlink(self: Self) bool {
+        return switch (self) {
+            .Symlink => true,
+            else => false,
         };
     }
 };
@@ -109,6 +136,7 @@ pub const FileSystem = struct {
     ///     IN node: *const DirNode - The directory under which to open the file/dir from
     ///     IN name: []const u8 - The name of the file to open
     ///     IN flags: OpenFlags - The flags to consult when opening the file
+    ///     IN args: OpenArgs - The arguments to use when creating a node
     ///
     /// Return: *const Node
     ///     The node representing the file/dir opened
@@ -116,8 +144,9 @@ pub const FileSystem = struct {
     /// Error: Allocator.Error || Error
     ///     Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request
     ///     Error.NoSuchFileOrDir - The file/dir by that name doesn't exist and the flags didn't specify to create it
+    ///     Error.NoSymlinkTarget - A symlink was created but no symlink target was provided in the args
     ///
-    const Open = fn (self: *const Self, node: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node;
+    const Open = fn (self: *const Self, node: *const DirNode, name: []const u8, flags: OpenFlags, args: OpenArgs) (Allocator.Error || Error)!*Node;
 
     ///
     /// Get the node representing the root of the filesystem. Used when mounting to bind the mount point to the root of the mounted fs
@@ -180,14 +209,14 @@ pub const DirNode = struct {
     mount: ?*const DirNode,
 
     /// See the documentation for FileSystem.Open
-    pub fn open(self: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node {
+    pub fn open(self: *const DirNode, name: []const u8, flags: OpenFlags, args: OpenArgs) (Allocator.Error || Error)!*Node {
         var fs = self.fs;
         var node = self;
         if (self.mount) |mnt| {
             fs = mnt.fs;
             node = mnt;
         }
-        return fs.open(fs, node, name, flags);
+        return fs.open(fs, node, name, flags, args);
     }
 };
 
@@ -202,6 +231,9 @@ pub const Error = error{
     /// The requested file is actually a directory
     IsADirectory,
 
+    /// The requested symlink is actually a file
+    IsAFile,
+
     /// The path provided is not absolute
     NotAbsolutePath,
 
@@ -210,6 +242,9 @@ pub const Error = error{
 
     /// The node is not recognised as being opened by the filesystem
     NotOpened,
+
+    /// No symlink target was provided when one was expected
+    NoSymlinkTarget,
 };
 
 /// Errors that can be thrown when attempting to mount
@@ -230,6 +265,7 @@ var root: *Node = undefined;
 /// Arguments:
 ///     IN path: []const u8 - The path to traverse. Must be absolute (see isAbsolute)
 ///     IN flags: OpenFlags - The flags that specify if the file/dir should be created if it doesn't exist
+///     IN args: OpenArgs - The extra args needed when creating new nodes.
 ///
 /// Return: *const Node
 ///     The node that exists at the path starting at the system root
@@ -238,8 +274,9 @@ var root: *Node = undefined;
 ///     Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request
 ///     Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory
 ///     Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it
+///     Error.NoSymlinkTarget - A non-null symlink target was not provided when creating a symlink
 ///
-fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node {
+fn traversePath(path: []const u8, follow_symlinks: bool, flags: OpenFlags, args: OpenArgs) (Allocator.Error || Error)!*Node {
     if (!isAbsolute(path)) {
         return Error.NotAbsolutePath;
     }
@@ -250,7 +287,7 @@ fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*
 
         const Self = @This();
 
-        fn func(split: *std.mem.SplitIterator, node: *Node, rec_flags: OpenFlags) (Allocator.Error || Error)!Self {
+        fn func(split: *std.mem.SplitIterator, node: *Node, follow_links: bool, rec_flags: OpenFlags) (Allocator.Error || Error)!Self {
             // Get segment string. This will not be unreachable as we've made sure the spliterator has more segments left
             const seg = split.next() orelse unreachable;
             if (split.rest().len == 0) {
@@ -261,7 +298,15 @@ fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*
             }
             return switch (node.*) {
                 .File => Error.NotADirectory,
-                .Dir => |*dir| try func(split, try dir.open(seg, rec_flags), rec_flags),
+                .Dir => |*dir| blk: {
+                    var child = try dir.open(seg, rec_flags, .{});
+                    // If the segment refers to a symlink, redirect to the node it represents instead
+                    if (child.isSymlink() and follow_links) {
+                        child = try traversePath(child.Symlink, follow_links, rec_flags, .{});
+                    }
+                    break :blk try func(split, child, follow_links, rec_flags);
+                },
+                .Symlink => |target| if (follow_links) try func(split, try traversePath(target, follow_links, .NO_CREATION, .{}), follow_links, rec_flags) else Error.NotADirectory,
             };
         }
     };
@@ -269,7 +314,7 @@ fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*
     // Split path but skip the first separator character
     var split = std.mem.split(path[1..], &[_]u8{SEPARATOR});
     // Traverse directories while we're not at the last segment
-    const result = try TraversalParent.func(&split, root, .NO_CREATION);
+    const result = try TraversalParent.func(&split, root, follow_symlinks, .NO_CREATION);
 
     // There won't always be a second segment in the path, e.g. in "/"
     if (std.mem.eql(u8, result.child, "")) {
@@ -282,7 +327,15 @@ fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*
             file.close();
             break :blk Error.NotADirectory;
         },
-        .Dir => |*dir| try dir.open(result.child, flags),
+        .Symlink => |target| if (follow_symlinks) try traversePath(target, follow_symlinks, .NO_CREATION, .{}) else result.parent,
+        .Dir => |*dir| blk: {
+            var n = try dir.open(result.child, flags, args);
+            if (n.isSymlink() and follow_symlinks) {
+                // If the child is a symnlink and we're following them, find the node it refers to
+                n = try traversePath(n.Symlink, follow_symlinks, flags, args);
+            }
+            break :blk n;
+        },
     };
 }
 
@@ -308,7 +361,9 @@ pub fn mount(dir: *DirNode, fs: *const FileSystem) MountError!void {
 ///
 /// Arguments:
 ///     IN path: []const u8 - The path to open. Must be absolute (see isAbsolute)
+///     IN follow_symlinks: bool - Whether symbolic links should be followed. When this is false and the path traversal encounters a symlink before the end segment of the path, NotADirectory is returned.
 ///     IN flags: OpenFlags - The flags specifying if this node should be created if it doesn't exist
+///     IN args: OpenArgs - The extra args needed when creating new nodes.
 ///
 /// Return: *const Node
 ///     The node that exists at the path starting at the system root
@@ -317,9 +372,10 @@ pub fn mount(dir: *DirNode, fs: *const FileSystem) MountError!void {
 ///     Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request
 ///     Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory
 ///     Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it
+///     Error.NoSymlinkTarget - A non-null symlink target was not provided when creating a symlink
 ///
-pub fn open(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node {
-    return try traversePath(path, flags);
+pub fn open(path: []const u8, follow_symlinks: bool, flags: OpenFlags, args: OpenArgs) (Allocator.Error || Error)!*Node {
+    return try traversePath(path, follow_symlinks, flags, args);
 }
 
 ///
@@ -341,13 +397,15 @@ pub fn open(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node
 ///
 pub fn openFile(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*const FileNode {
     switch (flags) {
-        .CREATE_DIR => return Error.InvalidFlags,
+        .CREATE_DIR, .CREATE_SYMLINK => return Error.InvalidFlags,
         .NO_CREATION, .CREATE_FILE => {},
     }
-    var node = try open(path, flags);
+    var node = try open(path, true, flags, .{});
     return switch (node.*) {
         .File => &node.File,
         .Dir => Error.IsADirectory,
+        // We instructed open to folow symlinks above, so this is impossible
+        .Symlink => unreachable,
     };
 }
 
@@ -370,19 +428,53 @@ pub fn openFile(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*
 ///
 pub fn openDir(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*DirNode {
     switch (flags) {
-        .CREATE_FILE => return Error.InvalidFlags,
+        .CREATE_FILE, .CREATE_SYMLINK => return Error.InvalidFlags,
         .NO_CREATION, .CREATE_DIR => {},
     }
-    var node = try open(path, flags);
+    var node = try open(path, true, flags, .{});
     return switch (node.*) {
         .File => |*file| blk: {
             file.close();
             break :blk Error.NotADirectory;
         },
+        // We instructed open to folow symlinks above, so this is impossible
+        .Symlink => unreachable,
         .Dir => &node.Dir,
     };
 }
 
+///
+/// Open a symlink at a path with a target.
+///
+/// Arguments:
+///     IN path: []const u8 - The path to open. Must be absolute (see isAbsolute)
+///     IN tareget: ?[]const u8 - The target to use when creating the symlink. Can be null if .NO_CREATION is used as the open flag
+///     IN flags: OpenFlags - The flags specifying if this node should be created if it doesn't exist. Cannot be CREATE_FILE
+///
+/// Return: []const u8
+///     The opened symlink's target
+///
+/// Error: Allocator.Error || Error
+///     Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request
+///     Error.InvalidFlags - The flags were a value invalid when opening a symlink
+///     Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory
+///     Error.NoSuchFileOrDir - The symlink at the end of the path doesn't exist and the flags didn't specify to create it
+///     Error.IsAFile - The path corresponds to a file rather than a symlink
+///     Error.IsADirectory - The path corresponds to a directory rather than a symlink
+///
+pub fn openSymlink(path: []const u8, target: ?[]const u8, flags: OpenFlags) (Allocator.Error || Error)![]const u8 {
+    switch (flags) {
+        .CREATE_DIR, .CREATE_FILE => return Error.InvalidFlags,
+        .NO_CREATION, .CREATE_SYMLINK => {},
+    }
+    var node = try open(path, false, flags, .{ .symlink_target = target });
+    return switch (node.*) {
+        .Symlink => |t| t,
+        .File => Error.IsAFile,
+        .Dir => Error.IsADirectory,
+    };
+}
+
 // TODO: Replace this with the std lib implementation once the OS abstraction layer is up and running
 ///
 /// Check if a path is absolute, i.e. its length is greater than 0 and starts with the path separator character
@@ -505,7 +597,7 @@ const TestFS = struct {
         return bytes.len;
     }
 
-    fn open(fs: *const FileSystem, dir: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node {
+    fn open(fs: *const FileSystem, dir: *const DirNode, name: []const u8, flags: OpenFlags, args: OpenArgs) (Allocator.Error || Error)!*Node {
         var test_fs = @fieldParentPtr(TestFS, "instance", fs.instance);
         const parent = (try getTreeNode(test_fs, dir)) orelse unreachable;
         // Check if the children match the file wanted
@@ -532,6 +624,14 @@ const TestFS = struct {
                     child = try test_fs.allocator.create(Node);
                     child.* = .{ .File = .{ .fs = test_fs.fs } };
                 },
+                .CREATE_SYMLINK => {
+                    if (args.symlink_target) |target| {
+                        child = try test_fs.allocator.create(Node);
+                        child.* = .{ .Symlink = target };
+                    } else {
+                        return Error.NoSymlinkTarget;
+                    }
+                },
                 .NO_CREATION => unreachable,
             }
             // Create the test fs tree node
@@ -625,34 +725,46 @@ test "traversePath" {
     root = testfs.tree.val;
 
     // Get the root
-    var test_root = try traversePath("/", .NO_CREATION);
+    var test_root = try traversePath("/", false, .NO_CREATION, .{});
     testing.expectEqual(test_root, root);
     // Create a file in the root and try to traverse to it
-    var child1 = try test_root.Dir.open("child1.txt", .CREATE_FILE);
-    var child1_traversed = try traversePath("/child1.txt", .NO_CREATION);
+    var child1 = try test_root.Dir.open("child1.txt", .CREATE_FILE, .{});
+    var child1_traversed = try traversePath("/child1.txt", false, .NO_CREATION, .{});
     testing.expectEqual(child1, child1_traversed);
     // Close the open files
     child1.File.close();
     child1_traversed.File.close();
 
     // Same but with a directory
-    var child2 = try test_root.Dir.open("child2", .CREATE_DIR);
-    testing.expectEqual(child2, try traversePath("/child2", .NO_CREATION));
+    var child2 = try test_root.Dir.open("child2", .CREATE_DIR, .{});
+    testing.expectEqual(child2, try traversePath("/child2", false, .NO_CREATION, .{}));
 
     // Again but with a file within that directory
-    var child3 = try child2.Dir.open("child3.txt", .CREATE_FILE);
-    var child3_traversed = try traversePath("/child2/child3.txt", .NO_CREATION);
+    var child3 = try child2.Dir.open("child3.txt", .CREATE_FILE, .{});
+    var child3_traversed = try traversePath("/child2/child3.txt", false, .NO_CREATION, .{});
     testing.expectEqual(child3, child3_traversed);
     // Close the open files
     child3.File.close();
     child3_traversed.File.close();
 
-    testing.expectError(Error.NotAbsolutePath, traversePath("abc", .NO_CREATION));
-    testing.expectError(Error.NotAbsolutePath, traversePath("", .NO_CREATION));
-    testing.expectError(Error.NotAbsolutePath, traversePath("a/", .NO_CREATION));
-    testing.expectError(Error.NoSuchFileOrDir, traversePath("/notadir/abc.txt", .NO_CREATION));
-    testing.expectError(Error.NoSuchFileOrDir, traversePath("/ ", .NO_CREATION));
-    testing.expectError(Error.NotADirectory, traversePath("/child1.txt/abc.txt", .NO_CREATION));
+    // Create and open a symlink
+    var child4 = try traversePath("/child2/link", false, .CREATE_SYMLINK, .{ .symlink_target = "/child2/child3.txt" });
+    var child4_linked = try traversePath("/child2/link", true, .NO_CREATION, .{});
+    testing.expectEqual(child4_linked, child3);
+    var child5 = try traversePath("/child4", false, .CREATE_SYMLINK, .{ .symlink_target = "/child2" });
+    var child5_linked = try traversePath("/child4/child3.txt", true, .NO_CREATION, .{});
+    std.debug.warn("child5_linked {}, child4_linked {}\n", .{ child5_linked, child4_linked });
+    testing.expectEqual(child5_linked, child4_linked);
+    child4_linked.File.close();
+    child5_linked.File.close();
+
+    testing.expectError(Error.NotAbsolutePath, traversePath("abc", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NotAbsolutePath, traversePath("", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NotAbsolutePath, traversePath("a/", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NoSuchFileOrDir, traversePath("/notadir/abc.txt", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NoSuchFileOrDir, traversePath("/ ", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NotADirectory, traversePath("/child1.txt/abc.txt", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NoSymlinkTarget, traversePath("/childX.txt", false, .CREATE_SYMLINK, .{}));
 
     // Since we've closed all the files, the open files count should be zero
     testing.expectEqual(testfs.open_files_count, 0);
@@ -674,18 +786,32 @@ test "isDir" {
     const fs: FileSystem = undefined;
     const dir = Node{ .Dir = .{ .fs = &fs, .mount = null } };
     const file = Node{ .File = .{ .fs = &fs } };
-    testing.expect(dir.isDir());
+    const symlink = Node{ .Symlink = "" };
+    testing.expect(!symlink.isDir());
     testing.expect(!file.isDir());
+    testing.expect(dir.isDir());
 }
 
 test "isFile" {
     const fs: FileSystem = undefined;
     const dir = Node{ .Dir = .{ .fs = &fs, .mount = null } };
     const file = Node{ .File = .{ .fs = &fs } };
+    const symlink = Node{ .Symlink = "" };
     testing.expect(!dir.isFile());
+    testing.expect(!symlink.isFile());
     testing.expect(file.isFile());
 }
 
+test "isSymlink" {
+    const fs: FileSystem = undefined;
+    const dir = Node{ .Dir = .{ .fs = &fs, .mount = null } };
+    const file = Node{ .File = .{ .fs = &fs } };
+    const symlink = Node{ .Symlink = "" };
+    testing.expect(!dir.isSymlink());
+    testing.expect(!file.isSymlink());
+    testing.expect(symlink.isSymlink());
+}
+
 test "open" {
     var testfs = try testInitFs(testing.allocator);
     defer testing.allocator.destroy(testfs);
@@ -731,8 +857,9 @@ test "open" {
     testing.expectError(Error.IsADirectory, openFile("/def", .NO_CREATION));
     testing.expectError(Error.InvalidFlags, openFile("/abc.txt", .CREATE_DIR));
     testing.expectError(Error.InvalidFlags, openDir("/abc.txt", .CREATE_FILE));
-    testing.expectError(Error.NotAbsolutePath, open("", .NO_CREATION));
-    testing.expectError(Error.NotAbsolutePath, open("abc", .NO_CREATION));
+    testing.expectError(Error.NotAbsolutePath, open("", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NotAbsolutePath, open("abc", false, .NO_CREATION, .{}));
+    testing.expectError(Error.NoSymlinkTarget, open("/abc", false, .CREATE_SYMLINK, .{}));
 }
 
 test "read" {
@@ -771,6 +898,14 @@ test "read" {
         const length = try test_file.read(buffer[0..0]);
         testing.expect(std.mem.eql(u8, str[0..0], buffer[0..length]));
     }
+    // Try reading from a symlink
+    var test_link = try openSymlink("/link", "/foo.txt", .CREATE_SYMLINK);
+    testing.expectEqual(test_link, "/foo.txt");
+    var link_file = try openFile("/link", .NO_CREATION);
+    {
+        const length = try link_file.read(buffer[0..0]);
+        testing.expect(std.mem.eql(u8, str[0..0], buffer[0..length]));
+    }
 }
 
 test "write" {
@@ -787,4 +922,13 @@ test "write" {
     const length = try test_file.write(str);
     testing.expect(std.mem.eql(u8, str, f_data.* orelse unreachable));
     testing.expect(length == str.len);
+
+    // Try writing to a symlink
+    var test_link = try openSymlink("/link", "/foo.txt", .CREATE_SYMLINK);
+    testing.expectEqual(test_link, "/foo.txt");
+    var link_file = try openFile("/link", .NO_CREATION);
+
+    var str2 = "test456";
+    const length2 = try test_file.write(str2);
+    testing.expect(std.mem.eql(u8, str2, f_data.* orelse unreachable));
 }