zig/lib/std/fs/wasi.zig
InKryption bc72ae5e4e Sema: Prevent coercion from tuple pointer to mutable slice.
Also fix some stdlib code affected by this.

Co-authored by: topolarity <topolarity@tapscott.me>
2022-10-27 22:00:47 -04:00

297 lines
11 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const os = std.os;
const mem = std.mem;
const math = std.math;
const fs = std.fs;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const wasi = std.os.wasi;
const fd_t = wasi.fd_t;
const prestat_t = wasi.prestat_t;
/// Type-tag of WASI preopen.
///
/// WASI currently offers only `Dir` as a valid preopen resource.
pub const PreopenTypeTag = enum {
Dir,
};
/// Type of WASI preopen.
///
/// WASI currently offers only `Dir` as a valid preopen resource.
pub const PreopenType = union(PreopenTypeTag) {
/// Preopened directory type.
Dir: []const u8,
const Self = @This();
pub fn eql(self: Self, other: PreopenType) bool {
if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false;
switch (self) {
PreopenTypeTag.Dir => |this_path| return mem.eql(u8, this_path, other.Dir),
}
}
// Checks whether `other` refers to a subdirectory of `self` and, if so,
// returns the relative path to `other` from `self`
//
// Expects `other` to be a canonical path, not containing "." or ".."
pub fn getRelativePath(self: Self, other: PreopenType) ?[]const u8 {
if (std.meta.activeTag(self) != std.meta.activeTag(other)) return null;
switch (self) {
PreopenTypeTag.Dir => |self_path| {
const other_path = other.Dir;
if (mem.indexOfDiff(u8, self_path, other_path)) |index| {
if (index < self_path.len) return null;
}
const rel_path = other_path[self_path.len..];
if (rel_path.len == 0) {
return rel_path;
} else if (rel_path[0] == '/') {
return rel_path[1..];
} else {
if (self_path[self_path.len - 1] != '/') return null;
return rel_path;
}
},
}
}
pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void {
_ = fmt;
_ = options;
try out_stream.print("PreopenType{{ ", .{});
switch (self) {
PreopenType.Dir => |path| try out_stream.print(".Dir = '{}'", .{std.zig.fmtId(path)}),
}
return out_stream.print(" }}", .{});
}
};
/// WASI preopen struct. This struct consists of a WASI file descriptor
/// and type of WASI preopen. It can be obtained directly from the WASI
/// runtime using `PreopenList.populate()` method.
pub const Preopen = struct {
/// WASI file descriptor.
fd: fd_t,
/// Type of the preopen.
@"type": PreopenType,
/// Construct new `Preopen` instance.
pub fn new(fd: fd_t, preopen_type: PreopenType) Preopen {
return Preopen{
.fd = fd,
.@"type" = preopen_type,
};
}
};
/// WASI resource identifier struct. This is effectively a path within
/// a WASI Preopen.
pub const PreopenUri = struct {
/// WASI Preopen containing the resource.
base: Preopen,
/// Path to resource within `base`.
relative_path: []const u8,
};
/// Dynamically-sized array list of WASI preopens. This struct is a
/// convenience wrapper for issuing `std.os.wasi.fd_prestat_get` and
/// `std.os.wasi.fd_prestat_dir_name` syscalls to the WASI runtime, and
/// collecting the returned preopens.
///
/// This struct is intended to be used in any WASI program which intends
/// to use the capabilities as passed on by the user of the runtime.
pub const PreopenList = struct {
const InnerList = std.ArrayList(Preopen);
/// Internal dynamically-sized buffer for storing the gathered preopens.
buffer: InnerList,
const Self = @This();
pub const Error = error{ OutOfMemory, Overflow } || os.UnexpectedError;
/// Deinitialize with `deinit`.
pub fn init(allocator: Allocator) Self {
return Self{ .buffer = InnerList.init(allocator) };
}
/// Release all allocated memory.
pub fn deinit(pm: Self) void {
for (pm.buffer.items) |preopen| {
switch (preopen.@"type") {
PreopenType.Dir => |path| pm.buffer.allocator.free(path),
}
}
pm.buffer.deinit();
}
/// Populate the list with the preopens by issuing `std.os.wasi.fd_prestat_get`
/// and `std.os.wasi.fd_prestat_dir_name` syscalls to the runtime.
///
/// If called more than once, it will clear its contents every time before
/// issuing the syscalls.
///
/// In the unlinkely event of overflowing the number of available file descriptors,
/// returns `error.Overflow`. In this case, even though an error condition was reached
/// the preopen list still contains all valid preopened file descriptors that are valid
/// for use. Therefore, it is fine to call `find`, `asSlice`, or `toOwnedSlice`. Finally,
/// `deinit` still must be called!
///
/// Usage of `cwd_root`:
/// If provided, `cwd_root` is inserted as prefix for any Preopens that
/// begin with "." and all paths are normalized as POSIX-style absolute
/// paths. `cwd_root` must be an absolute path.
///
/// For example:
/// "./foo/bar" -> "{cwd_root}/foo/bar"
/// "foo/bar" -> "/foo/bar"
/// "/foo/bar" -> "/foo/bar"
///
/// If `cwd_root` is not provided, all preopen directories are unmodified.
///
pub fn populate(self: *Self, cwd_root: ?[]const u8) Error!void {
if (cwd_root) |root| assert(fs.path.isAbsolute(root));
// Clear contents if we're being called again
for (self.toOwnedSlice()) |preopen| {
switch (preopen.@"type") {
PreopenType.Dir => |path| self.buffer.allocator.free(path),
}
}
errdefer self.deinit();
var fd: fd_t = 3; // start fd has to be beyond stdio fds
var path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
while (true) {
var buf: prestat_t = undefined;
switch (wasi.fd_prestat_get(fd, &buf)) {
.SUCCESS => {},
.OPNOTSUPP => {
// not a preopen, so keep going
fd = try math.add(fd_t, fd, 1);
continue;
},
.BADF => {
// OK, no more fds available
break;
},
else => |err| return os.unexpectedErrno(err),
}
const preopen_len = buf.u.dir.pr_name_len;
mem.set(u8, path_buf[0..preopen_len], 0);
switch (wasi.fd_prestat_dir_name(fd, &path_buf, preopen_len)) {
.SUCCESS => {},
else => |err| return os.unexpectedErrno(err),
}
// Unfortunately, WASI runtimes (e.g. wasmer) are not consistent about whether the
// NULL sentinel is included in the reported Preopen name_len
const raw_path = if (path_buf[preopen_len - 1] == 0) blk: {
break :blk path_buf[0 .. preopen_len - 1];
} else path_buf[0..preopen_len];
// If we were provided a CWD root to resolve against, we try to treat Preopen dirs as
// POSIX paths, relative to "/" or `cwd_root` depending on whether they start with "."
const path = if (cwd_root) |cwd| blk: {
const resolve_paths: []const []const u8 = if (raw_path[0] == '.') &.{ cwd, raw_path } else &.{ "/", raw_path };
break :blk fs.path.resolve(self.buffer.allocator, resolve_paths) catch |err| switch (err) {
error.CurrentWorkingDirectoryUnlinked => unreachable, // root is absolute, so CWD not queried
else => |e| return e,
};
} else blk: {
// If we were provided no CWD root, we preserve the preopen dir without resolving
break :blk try self.buffer.allocator.dupe(u8, raw_path);
};
errdefer self.buffer.allocator.free(path);
const preopen = Preopen.new(fd, .{ .Dir = path });
try self.buffer.append(preopen);
fd = try math.add(fd_t, fd, 1);
}
}
/// Find a preopen which includes access to `preopen_type`.
///
/// If multiple preopens match the provided resource, the most specific
/// match is returned. More recent preopens take priority, as well.
pub fn findContaining(self: Self, preopen_type: PreopenType) ?PreopenUri {
var best_match: ?PreopenUri = null;
for (self.buffer.items) |preopen| {
if (preopen.@"type".getRelativePath(preopen_type)) |rel_path| {
if (best_match == null or rel_path.len <= best_match.?.relative_path.len) {
best_match = PreopenUri{
.base = preopen,
.relative_path = if (rel_path.len == 0) "." else rel_path,
};
}
}
}
return best_match;
}
/// Find preopen by fd. If the preopen exists, return it.
/// Otherwise, return `null`.
pub fn findByFd(self: Self, fd: fd_t) ?Preopen {
for (self.buffer.items) |preopen| {
if (preopen.fd == fd) {
return preopen;
}
}
return null;
}
/// Find preopen by type. If the preopen exists, return it.
/// Otherwise, return `null`.
pub fn find(self: Self, preopen_type: PreopenType) ?*const Preopen {
for (self.buffer.items) |*preopen| {
if (preopen.@"type".eql(preopen_type)) {
return preopen;
}
}
return null;
}
/// Return the inner buffer as read-only slice.
pub fn asSlice(self: Self) []const Preopen {
return self.buffer.items;
}
/// The caller owns the returned memory. ArrayList becomes empty.
pub fn toOwnedSlice(self: *Self) []Preopen {
return self.buffer.toOwnedSlice();
}
};
test "extracting WASI preopens" {
if (builtin.os.tag != .wasi or builtin.link_libc) return error.SkipZigTest;
var preopens = PreopenList.init(std.testing.allocator);
defer preopens.deinit();
try preopens.populate(null);
const preopen = preopens.find(PreopenType{ .Dir = "." }) orelse unreachable;
try std.testing.expect(preopen.@"type".eql(PreopenType{ .Dir = "." }));
const po_type1 = PreopenType{ .Dir = "/" };
try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/" }).?, ""));
try std.testing.expect(std.mem.eql(u8, po_type1.getRelativePath(.{ .Dir = "/test/foobar" }).?, "test/foobar"));
const po_type2 = PreopenType{ .Dir = "/test/foo" };
try std.testing.expect(po_type2.getRelativePath(.{ .Dir = "/test/foobar" }) == null);
const po_type3 = PreopenType{ .Dir = "/test" };
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test" }).?, ""));
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/" }).?, ""));
try std.testing.expect(std.mem.eql(u8, po_type3.getRelativePath(.{ .Dir = "/test/foo/bar" }).?, "foo/bar"));
}