// SPDX-FileCopyrightText: 2024 Himbeer // // SPDX-License-Identifier: AGPL-3.0-or-later const std = @import("std"); const mem = @import("mem.zig"); const paging = @import("paging.zig"); const process = @import("process.zig"); const sysexchange = @import("sysexchange.zig"); const Allocator = std.mem.Allocator; const Result = sysexchange.Result; const TermHook = process.Info.TermHook; var root: Node = .{ .data = .{ .name_ptr = "", .name_len = 0, .inode = .{ .resource = .{ .tag = .dir, .data = .{ .dir = undefined }, }, .pid = 0, .options = .{ .reclaimable = false, }, .flags = .{}, }, }, }; pub fn treeRoot() *const Tree { return root.data.inode.resource.data.dir; } pub const Error = error{ NotFound, RelativePathNotAllowed, NotADirectory, NoAbsoluteContainingDirectory, TooManyReferences, ReadNotSupported, WriteNotSupported, InUse, Detached, Orphaned, AlreadyExists, IsAHook, IsAContainer, ProvideNotSupported, RemoveNotSupported, }; // A stream is a resource that provides a shared data stream with a driver. pub const Stream = extern struct { readFn: ?ReadFn, writeFn: ?WriteFn, pub const ReadFn = *const fn (ptr: [*]u8, len: usize) callconv(.C) Result(usize); pub const WriteFn = *const fn (ptr: [*]const u8, len: usize) callconv(.C) Result(usize); }; // A file is a resource that creates a unique data stream with a driver. pub const File = extern struct { readFn: ?ReadFn, writeFn: ?WriteFn, closeFn: ?CloseFn, initializer: ?*anyopaque, pub const ReadFn = *const fn (context: *FileContext, ptr: [*]u8, len: usize) callconv(.C) Result(usize); pub const WriteFn = *const fn (context: *FileContext, ptr: [*]const u8, len: usize) callconv(.C) Result(usize); pub const CloseFn = *const fn (context: *FileContext) callconv(.C) void; }; // A hook is a resource that invokes raw driver callbacks when interacted with. pub const Hook = extern struct { callback: Callback, pub const Callback = *allowzero const fn (pid: u16, thread_id: usize, data: usize) callconv(.C) Result(usize); }; // A directory hook is a resource that provides other resources via driver callbacks. pub const DirHook = extern struct { provideFn: ?ProvideFn, findFn: FindFn, listFn: ListFn, removeFn: ?RemoveFn, pub const ProvideFn = *const fn (name_ptr: [*]const u8, name_len: usize, inode: Inode) callconv(.C) Result(void); pub const FindFn = *allowzero const fn (name_ptr: [*]const u8, name_len: usize) callconv(.C) ?*Inode; pub const ListFn = *allowzero const fn (entries_ptr: [*]DirEntry, entries_len: usize) callconv(.C) Result(usize); pub const RemoveFn = *const fn (name_ptr: [*]const u8, name_len: usize) callconv(.C) Result(void); }; pub const FileContext = extern struct { inner: ?*anyopaque, initializer: ?*anyopaque, }; pub const ResourceKind = enum(u32) { stream, file, hook, dir, dir_hook, }; pub const Resource = extern struct { tag: ResourceKind, data: extern union { stream: Stream, file: File, hook: Hook, dir: *Tree, dir_hook: DirHook, }, }; pub const Inode = extern struct { resource: Resource, refs: usize = 0, pid: u16, options: Options, flags: Flags, }; pub const Options = extern struct { reclaimable: bool, }; pub const Flags = extern struct { detached: bool = false, }; pub const DirEntry = extern struct { name_ptr: [*]const u8, name_len: usize, inode: Inode, pub fn name(self: DirEntry) []const u8 { return self.name_ptr[0..self.name_len]; } }; pub const Node = std.DoublyLinkedList(DirEntry).Node; pub const Tree = struct { allocator: Allocator, nodes: std.DoublyLinkedList(DirEntry), pub fn init(allocator: Allocator) Tree { return .{ .allocator = allocator, .nodes = std.DoublyLinkedList(DirEntry){}, }; } pub fn find(self: *const Tree, name: []const u8) ?*Node { var node = self.nodes.first; while (node) |current_node| { if (current_node.next == current_node) break; if (std.mem.eql(u8, current_node.data.name(), name)) return current_node; node = current_node.next; } return null; } pub fn provideResource(self: *Tree, entry: DirEntry) !void { var node = try self.allocator.create(Node); node.data = entry; self.nodes.append(node); } pub fn remove(self: *Tree, name: []const u8) !void { if (self.find(name)) |node| { if (node.data.inode.refs > 0) return Error.InUse; self.nodes.remove(node); } else return Error.NotFound; } }; pub const OpenResult = union(enum) { rd: ResourceDescriptor, data: usize, }; pub const ResourceDescriptor = struct { inode: *Inode, context: ?*FileContext, pub fn init(allocator: Allocator, inode: *Inode) !ResourceDescriptor { inode.refs = std.math.add(usize, inode.refs, 1) catch return Error.TooManyReferences; if (inode.resource.tag == .file) { const context = try allocator.create(FileContext); context.inner = null; context.initializer = inode.resource.data.file.initializer; return .{ .inode = inode, .context = context, }; } return .{ .inode = inode, .context = null, }; } pub fn deinit(self: ResourceDescriptor) void { self.inode.refs -|= 1; if (self.inode.resource.tag == .file) { if (self.inode.resource.data.file.closeFn) |closeFn| { if (self.inode.pid == 0) { closeFn(self.context.?); return; } const driver = process.latestThread(self.inode.pid).?; call(driver, closeFn, .{self.context.?}, null, null) catch {}; } } } pub fn read(self: ResourceDescriptor, proc: *process.Info, buffer: []u8) !usize { return readHooked(self, proc, buffer, .{ .hookFn = crossProcessReturn, .context = proc, }); } pub fn readHooked(self: ResourceDescriptor, proc: *process.Info, buffer: []u8, hook: TermHook) !usize { if (self.inode.flags.detached and self.inode.options.reclaimable) { return Error.Detached; } if (self.inode.flags.detached and !self.inode.options.reclaimable) { return Error.Orphaned; } return switch (self.inode.resource.tag) { .stream => self.readStream(self.inode.resource.data.stream, proc, buffer, hook), .file => self.readFile(self.inode.resource.data.file, proc, buffer, hook), else => Error.ReadNotSupported, }; } fn readStream(self: ResourceDescriptor, stream: Stream, proc: *process.Info, buffer: []u8, hook: TermHook) !usize { const readFn = stream.readFn orelse return Error.ReadNotSupported; if (self.inode.pid == 0) { return readFn(buffer.ptr, buffer.len).toErrorUnion(); } const driver = process.latestThread(self.inode.pid).?; const copy = try driver.copyBuffer(buffer); proc.state = .suspended; try call(driver, readFn, .{ copy.ptr, copy.len }, .{ .cleanupFn = moveBack, .buffer = buffer, .copy = copy, }, hook); } fn readFile(self: ResourceDescriptor, file: File, proc: *process.Info, buffer: []u8, hook: TermHook) !usize { const readFn = file.readFn orelse return Error.ReadNotSupported; if (self.inode.pid == 0) { return readFn(self.context.?, buffer.ptr, buffer.len).toErrorUnion(); } const driver = process.latestThread(self.inode.pid).?; const copy = try driver.copyBuffer(buffer); proc.state = .suspended; try call(driver, readFn, .{ self.context.?, copy.ptr, copy.len }, .{ .cleanupFn = moveBack, .buffer = buffer, .copy = copy, }, hook); } pub fn write(self: ResourceDescriptor, proc: *process.Info, bytes: []const u8) !usize { return writeHooked(self, proc, bytes, .{ .hookFn = crossProcessReturn, .context = proc, }); } pub fn writeHooked(self: ResourceDescriptor, proc: *process.Info, bytes: []const u8, hook: TermHook) !usize { if (self.inode.flags.detached and self.inode.options.reclaimable) { return Error.Detached; } if (self.inode.flags.detached and !self.inode.options.reclaimable) { return Error.Orphaned; } return switch (self.inode.resource.tag) { .stream => self.writeStream(self.inode.resource.data.stream, proc, bytes, hook), .file => self.writeFile(self.inode.resource.data.file, proc, bytes, hook), else => Error.WriteNotSupported, }; } fn writeStream(self: ResourceDescriptor, stream: Stream, proc: *process.Info, bytes: []const u8, hook: TermHook) !usize { const writeFn = stream.writeFn orelse return Error.WriteNotSupported; if (self.inode.pid == 0) { return writeFn(bytes.ptr, bytes.len).toErrorUnion(); } const driver = process.latestThread(self.inode.pid).?; const copy = try driver.copyBytes(bytes); proc.state = .suspended; try call(driver, writeFn, .{ copy.ptr, copy.len }, null, hook); } fn writeFile(self: ResourceDescriptor, file: File, proc: *process.Info, bytes: []const u8, hook: TermHook) !usize { const writeFn = file.writeFn orelse return Error.WriteNotSupported; if (self.inode.pid == 0) { return writeFn(self.context.?, bytes.ptr, bytes.len).toErrorUnion(); } const driver = process.latestThread(self.inode.pid).?; const copy = try driver.copyBytes(bytes); proc.state = .suspended; try call(driver, writeFn, .{ self.context.?, copy.ptr, copy.len }, null, hook); } }; pub const UserInfo = union(enum) { rd: ResourceDescriptor, value: Result(usize), }; pub fn init(allocator: Allocator) !void { const tree = try allocator.create(Tree); tree.* = Tree.init(allocator); root.data.inode.resource.data.dir = tree; } pub fn find(path: []const u8) ?*Inode { if (!std.fs.path.isAbsolutePosix(path)) return null; // Error set is empty. var it = std.fs.path.ComponentIterator(.posix, u8).init(path) catch unreachable; var resource = root.data.inode.resource; while (it.next()) |component| { const inode = switch (resource.tag) { .dir => blk: { const dir = resource.data.dir; break :blk if (dir.find(component.name)) |entry| &entry.data.inode else null; }, .dir_hook => blk: { // fixme: Doesn't call into U-mode drivers correctly const dir_hook = resource.data.dir_hook; break :blk dir_hook.findFn(component.name.ptr, component.name.len); }, else => null, // Not a directory (or directory hook). } orelse return null; if (std.mem.eql(u8, component.path, path)) { return inode; } resource = inode.resource; } // No components, this is the root directory (/). return &root.data.inode; } fn reclaim(node: *Node, resource: Resource, pid: u16, options: Options) !void { if (node.data.inode.flags.detached and node.data.inode.options.reclaimable) { node.data.inode = .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }; } else return Error.AlreadyExists; } fn provideInDir(dir: *Tree, name: []const u8, resource: Resource, pid: u16, options: Options) !void { if (dir.find(name)) |node| { return reclaim(node, resource, pid, options); } return dir.provideResource(.{ .name_ptr = name.ptr, .name_len = name.len, .inode = .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }, }); } fn provideInDirHook(dir_hook: DirHook, name: []const u8, resource: Resource, pid: u16, options: Options) !void { // fixme: Check for duplicate resources with the same path // fixme: Doesn't call into U-mode drivers correctly return if (dir_hook.provideFn) |provideFn| provideFn(name.ptr, name.len, .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }).toErrorUnion() else Error.ProvideNotSupported; } pub fn provideResource(path: []const u8, resource: Resource, pid: u16, options: Options) !void { if (!std.fs.path.isAbsolutePosix(path)) return Error.RelativePathNotAllowed; const dirname = std.fs.path.dirnamePosix(path) orelse return Error.NoAbsoluteContainingDirectory; const basename = std.fs.path.basenamePosix(path); const inode = find(dirname) orelse return Error.NotFound; return switch (inode.resource.tag) { .dir => provideInDir(inode.resource.data.dir, basename, resource, pid, options), .dir_hook => provideInDirHook(inode.resource.data.dir_hook, basename, resource, pid, options), else => Error.NotADirectory, }; } pub fn provideResourceZ(path_c: [*:0]const u8, resource: Resource, pid: u16, options: Options) !void { return provideResource(std.mem.sliceTo(path_c, 0), resource, pid, options); } fn callHook(hook: Hook, pid: u16, proc: *process.Info, data: usize) !usize { if (pid == 0) { return hook.callback(proc.id, proc.thread_id, data).toErrorUnion(); } const driver = process.latestThread(pid).?; proc.state = .suspended; try call(driver, hook.callback, .{ proc.id, proc.thread_id, data }, null, .{ .hookFn = crossProcessReturn, .context = proc, }); } pub fn open(proc: *process.Info, path: []const u8, data: usize) !OpenResult { const inode = find(path) orelse return Error.NotFound; if (inode.resource.tag == .hook) { const data_out = try callHook(inode.resource.data.hook, inode.pid, proc, data); return .{ .data = data_out }; } return .{ .rd = try ResourceDescriptor.init(mem.page_allocator, inode) }; } pub fn openZ(proc: *process.Info, path_c: [*:0]const u8, data: usize) !OpenResult { return open(proc, std.mem.sliceTo(path_c, 0), data); } pub fn openNonHook(path: []const u8) !ResourceDescriptor { const inode = find(path) orelse return Error.NotFound; if (inode.resource.tag == .hook) return Error.IsAHook; return ResourceDescriptor.init(mem.page_allocator, inode); } pub fn openNonHookZ(path_c: [*:0]const u8) !ResourceDescriptor { return openNonHook(std.mem.sliceTo(path_c, 0)); } fn listDir(dir: *Tree, entries: []DirEntry) !usize { paging.setUserMemoryAccess(true); defer paging.setUserMemoryAccess(false); var i: usize = 0; var next_node = dir.nodes.first; while (next_node) |node| : (next_node = node.next) { if (i >= entries.len) break; entries[i] = node.data; i += 1; } return @min(dir.nodes.len, entries.len); } fn listDirHook(proc: *process.Info, dir_hook: DirHook, pid: u16, entries: []DirEntry) !usize { return listDirHookHooked(proc, dir_hook, pid, entries, .{ .hookFn = crossProcessReturn, .context = proc, }); } fn listDirHookHooked(proc: *process.Info, dir_hook: DirHook, pid: u16, entries: []DirEntry, hook: TermHook) !usize { if (pid == 0) { return dir_hook.listFn(entries.ptr, entries.len).toErrorUnion(); } const entries_ptr: [*]u8 = @ptrCast(entries.ptr); const entries_bytes = entries_ptr[0 .. entries.len * @sizeOf(DirEntry)]; const driver = process.latestThread(pid).?; paging.setUserMemoryAccess(true); const copy = try driver.copyBuffer(entries_bytes); paging.setUserMemoryAccess(false); proc.state = .suspended; try call(driver, dir_hook.listFn, .{ copy.ptr, copy.len }, .{ .cleanupFn = moveBack, .buffer = entries_bytes, .copy = copy, }, hook); } pub fn list(proc: *process.Info, path: []const u8, entries: []DirEntry) !usize { const inode = find(path) orelse return Error.NotFound; return switch (inode.resource.tag) { .dir => listDir(inode.resource.data.dir, entries), .dir_hook => listDirHook(proc, inode.resource.data.dir_hook, inode.pid, entries), else => Error.NotADirectory, }; } pub fn listZ(proc: *process.Info, path_c: [*:0]const u8, entries: []DirEntry) !usize { return list(proc, std.mem.sliceTo(path_c, 0), entries); } fn call(proc: *process.Info, function: *const anyopaque, args: anytype, cleanup_hook: ?process.Info.CleanupHook, term_hook: ?process.Info.TermHook) !noreturn { const callback_thread = try proc.createThread(null); callback_thread.call(@intFromPtr(function), args, cleanup_hook, term_hook); } fn moveBack(driver: *const process.Info, buffer: []u8, copy: []const u8) void { paging.setUserMemoryAccess(true); @memcpy(buffer, copy); paging.setUserMemoryAccess(false); var addr = @intFromPtr(copy.ptr); const limit = addr + copy.len; while (addr < limit) : (addr += paging.page_size) { driver.page_table.unmapEntry(addr); } } fn crossProcessReturn(context: *anyopaque, driver: *const process.Info) void { const proc: *process.Info = @alignCast(@ptrCast(context)); proc.trap_frame.general_purpose_registers[10] = driver.trap_frame.general_purpose_registers[12]; proc.trap_frame.general_purpose_registers[11] = driver.trap_frame.general_purpose_registers[13]; proc.allowResume(); }