const ext = @cImport({ @cInclude("SDL2/SDL.h"); }); const mem = @import("./mem.zig"); const stack = @import("./stack.zig"); const std = @import("std"); /// /// /// pub const EventLoop = packed struct { current_request: ?*Request = null, /// /// With files typically being backed by a block device, they can produce a variety of errors - /// from physical to virtual errors - these are all encapsulated by the API as general /// [Error.Inaccessible] errors. /// pub const FileError = error { Inaccessible, }; /// /// [OpenError.NotFound] is used as a catch-all for any hardware or software-specific reason for /// failing to open a given file. This includes file-system restrictions surrounding a specific /// file as well as it simply not existing. /// /// [OpenError.OutOfFiles] occurs when there are no more resources available to open further /// files. As a result, some open files must be closed before more may be opened. /// pub const OpenError = error { NotFound, OutOfFiles, }; /// /// Indicates what kind of access the consumer logic has to a file. /// /// [OpenMode.read] is for reading from an existing file from the start. /// /// [OpenMode.overwrite] is for deleting the contents of a file, or creating an empty one if no /// such file exists, and writing to it from the start. /// /// [OpenMode.append] is for writing additional contents to a file, creating an empty one if no /// such file exists, on the end of whatever it already contains. /// pub const OpenMode = enum { read, overwrite, append, }; const Request = struct { next: ?*Request = null, frame: anyframe, message: union(enum) { close: struct { file: *ext.SDL_RWops, }, open: struct { path: *const Path, file: OpenError!*ext.SDL_RWops, mode: OpenMode, }, }, }; const max_files = 512; /// /// Asynchronously closes `file_access` via `event_loop`. /// /// *Note* that `file_access` must have been opened by `event_loop` for it to be closed by it, /// otherwise it will cause undefined behavior. /// pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void { var request = Request{ .frame = @frame(), .message = .{.close = @ptrCast(*ext.SDL_RWops, file_access)}, }; suspend { if (event_loop.current_request) |current_request| { current_request.next = &request; } else { event_loop.current_request = &request; } } } /// /// Asynchronously attempts to open access to a file at `path` via `event_loop`, with `mode` as /// the preferences for how it should be opened. /// /// A reference to a [FileAccess] representing the bound file is returned if the operation was /// successful, otherwise an [OpenError] if the file could not be opened. /// /// *Note* that, regardless of platform, files will always be treated as containing binary data. /// pub fn open(event_loop: *EventLoop, path: Path, mode: OpenMode) OpenError!*FileAccess { var request = Request{ .frame = @frame(), .message = .{ .open = .{ .path = &path, .file = error.OutOfFiles, .mode = mode, }, }, }; suspend { if (event_loop.current_request) |current_request| { current_request.next = &request; } else { event_loop.current_request = &request; } } return @ptrCast(*FileAccess, try request.message.open.file); } /// /// /// pub fn tick(event_loop: *EventLoop) void { while (event_loop.current_request) |request| { switch (request.message) { .close => |*close| { // Swallow file close errors. _ = ext.SDL_RWclose(close.file); }, .open => |*open| { open.file = ext.SDL_RWFromFile(&open.path.buffer, switch (open.mode) { .read => "rb", .overwrite => "wb", .append => "ab", }) orelse error.NotFound; }, } resume request.frame; event_loop.current_request = request.next; } } }; pub const FileAccess = opaque { }; /// /// Platform-agnostic mechanism for accessing files on any of the virtual file-systems supported by /// Ona. /// pub const Path = struct { locator: Locator, length: u16, buffer: [max]u8, /// /// Virtual file-system locators supported by Ona. /// pub const Locator = enum(u8) { relative, data, user, }; /// /// Errors that may occur during parsing of an incorrectly formatted [Path] URI. /// pub const ParseError = (JoinError || error { BadLocator, }); /// /// /// pub const JoinError = error { TooLong, }; /// /// Returns `true` if the length of `path` is empty, otherwise `false`. /// pub fn isEmpty(path: Path) bool { return (path.length == 0); } /// /// Attempts to lazily join `components` into a new [Path] value derived from `path`, returning /// it when `components` has no more data or a [JoinError] if the operation failed. /// /// Any duplicate path components, such as trailing ASCII forward-slashes (`/`) or periods /// (`.`), will be normalized to a more concise internal representation. /// /// *Note* that `components` may be mutated during execution of the operation. /// pub fn joinSpliterator(path: Path, components: *mem.Spliterator) JoinError!Path { var joined_path = path; var path_buffer = stack.Unmanaged{ .buffer = &joined_path.buffer, .filled = if (joined_path.length == 0) joined_path.length else (joined_path.length - 1), }; if (components.next()) |component| switch (component.len) { 0 => if (joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong, 1 => if ((component[0] == '.') and joined_path.isEmpty()) path_buffer.push("./") catch return error.TooLong, else => { if (!joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong; path_buffer.pushAll(component) catch return error.TooLong; if (components.hasNext()) path_buffer.push('/') catch return error.TooLong; }, }; while (components.next()) |component| if ((component.len != 0) or (!((component.len == 1) and (component[0] == '.')))) { if (!joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong; path_buffer.pushAll(component) catch return error.TooLong; if (components.hasNext()) path_buffer.push('/') catch return error.TooLong; }; // No space left over for the null terminator. if (path_buffer.filled >= max) return error.TooLong; joined_path.length = path_buffer.filled; return joined_path; } /// /// Returns `true` if `this` is equal to `that`, otherwise `false`. /// pub fn equals(this: Path, that: Path) bool { return (this.locator == that.locator) and std.mem.eql(this.buffer[0 .. this.length], that.buffer[0 .. that.length]); } /// /// Creates and returns an empty [Path] value rooted at the location of `locator`. /// pub fn from(locator: Locator) Path { return .{ .locator = locator, .length = 0, .buffer = std.mem.zeroes([max]u8), }; } /// /// The maximum possible byte-length of a [Path]. /// /// Note that paths are encoded using UTF-8, meaning that a character may be bigger than one /// byte. Because of this, it is not safe to asume that a path may hold [max] individual /// characters. /// pub const max = 1000; /// /// Attempts to parse the data in `sequence`, returning the [Path] value or an error from /// [ParseError] if it failed to parse. /// /// The rules surrounding the encoding of `sequence` are loose, with the only fundamental /// requirements being: /// /// * It starts with a locator key followed by an ASCII colon (`data:`, `user:`, etc.) /// followed by the rest of the path. /// /// * Each component of the path is separated by an ASCII forward-slash (`/`). /// /// * A path that begins with an ASCII forward-slash ('/') after the locator key is considered /// to be relative to the root of the specified locator key instead of relative to the path /// caller. /// /// Additional encoding rules are enforced via underlying file-system being used. For example, /// Microsoft Windows is case-insensitive while Unix and Linux systems are not. Additionally, /// Windows has far more reserved characters and sequences which cannot be used when interfacing /// with files compared to Linux and Unix systems. /// /// See [ParseError] for more information on the kinds of errors that may be returned. /// pub fn parse(sequence: []const u8) ParseError!Path { if (sequence.len == 0) return Path.from(.relative); if (mem.forwardFind(u8, sequence, ':')) |locator_path_delimiter_index| { var locator = std.meta.stringToEnum(Locator, sequence[0 .. locator_path_delimiter_index]) orelse return error.BadLocator; const components_index = (locator_path_delimiter_index + 1); return Path.from(locator).joinSpliterator(&.{ .source = sequence[components_index .. (sequence.len - components_index)], .delimiter = "/", }); } return Path.from(.relative).joinSpliterator(&.{ .source = sequence, .delimiter = "/", }); } };