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)); }