// 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 TermHook = process.Info.TermHook; var root: Node = .{ .data = .{ .name = "", .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, }; // 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) sysexchange.Result(usize); pub const WriteFn = *const fn (ptr: [*]const u8, len: usize) callconv(.C) sysexchange.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) sysexchange.Result(usize); pub const WriteFn = *const fn (context: *FileContext, ptr: [*]const u8, len: usize) callconv(.C) sysexchange.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) sysexchange.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 = *allowzero const fn (name_ptr: [*]const u8, name_len: usize, inode: Inode) callconv(.C) sysexchange.Result(void); pub const FindFn = *allowzero const fn (name_ptr: [*]const u8, name_len: usize) callconv(.C) ?*Inode; pub const ListFn = *allowzero const fn (inodes_ptr: [*]Inode, inodes_len: usize) callconv(.C) sysexchange.Result(usize); pub const RemoveFn = *allowzero const fn (name_ptr: [*]const u8, name_len: usize) callconv(.C) sysexchange.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 = struct { name: []const u8, inode: Inode, }; 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 => { const stream = self.inode.resource.data.stream; 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); }, .file => { const file = self.inode.resource.data.file; 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); }, else => Error.ReadNotSupported, }; } 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 => { const stream = self.inode.resource.data.stream; 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); }, .file => { const file = self.inode.resource.data.file; 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); }, else => Error.WriteNotSupported, }; } fn moveBack(driver: *const process.Info, buffer: []u8, copy: []const u8) void { paging.setUserMemoryAccess(true); defer paging.setUserMemoryAccess(false); @memcpy(buffer, copy); var addr = @intFromPtr(copy.ptr); const limit = addr + copy.len; while (addr < limit) : (addr += paging.page_size) { driver.page_table.unmapEntry(addr); } } }; pub const UserInfo = union(enum) { rd: ResourceDescriptor, value: sysexchange.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: { 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; } 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; if (find(dirname)) |inode| { const basename = std.fs.path.basenamePosix(path); return switch (inode.resource.tag) { .dir => blk: { const dir = inode.resource.data.dir; if (dir.find(basename)) |node| { if (node.data.inode.flags.detached and node.data.inode.options.reclaimable) { node.data.inode = .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }; break :blk; } else break :blk Error.AlreadyExists; } break :blk dir.provideResource(.{ .name = basename, .inode = .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }, }); }, .dir_hook => blk: { const dir_hook = inode.resource.data.dir_hook; // fixme: Check for duplicate resources with the same path break :blk dir_hook.provideFn(basename.ptr, basename.len, .{ .resource = resource, .pid = pid, .options = options, .flags = .{}, }).toErrorUnion(); }, else => Error.NotADirectory, }; } else return Error.NotFound; } 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); } pub fn open(proc: *process.Info, path: []const u8, data: usize) !OpenResult { const inode = find(path) orelse return Error.NotFound; return switch (inode.resource.tag) { .hook => blk: { const hook = inode.resource.data.hook; if (inode.pid == 0) { const result = hook.callback(proc.id, proc.thread_id, data); break :blk .{ .data = try result.toErrorUnion() }; } const driver = process.latestThread(inode.pid).?; proc.state = .suspended; try call(driver, hook.callback, .{ proc.id, proc.thread_id, data }, null, .{ .hookFn = crossProcessReturn, .context = proc, }); }, else => .{ .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 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 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(); }