Merge pull request #257 from ZystemOS/vfs-symlinks

Add symlink support
This commit is contained in:
Sam Tebbs 2020-11-05 18:26:25 +00:00 committed by GitHub
commit 374e95f322
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 190 additions and 39 deletions

View file

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

View file

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