From 9ae6e8b4a7bb6b21d8ee7710fc8b57f2a101b0e3 Mon Sep 17 00:00:00 2001 From: kayomn Date: Sun, 9 Oct 2022 23:10:13 +0100 Subject: [PATCH] Implement first pass of Oar archive reading mechanism --- src/main.zig | 16 +- src/math.zig | 6 + src/oar.zig | 14 + src/sys.zig | 1100 +++++++++++++++++++++++++++++++------------------- src/tar.zig | 29 -- 5 files changed, 714 insertions(+), 451 deletions(-) create mode 100644 src/math.zig create mode 100644 src/oar.zig delete mode 100644 src/tar.zig diff --git a/src/main.zig b/src/main.zig index df78f01..a3bcbf8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,29 +21,29 @@ test { _ = sys; } -fn run(ev: *sys.EventLoop, fs: *const sys.FileSystem, gr: *sys.GraphicsContext) anyerror!void { +fn run(app: *sys.App, graphics: *sys.GraphicsContext) anyerror!void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); { - const file_access = try ev.open(.readonly, try fs.data.joinedPath(&.{"ona.lua"})); + const file_access = try (try app.data().joinedPath(&.{"ona.lua"})).open(app, .readonly); - defer ev.close(file_access); + defer file_access.close(app); - const file_size = try file_access.size(ev); + const file_size = try file_access.queryLength(app); const allocator = gpa.allocator(); const buffer = try allocator.alloc(u8, file_size); defer allocator.free(buffer); - if ((try ev.readFile(file_access, buffer)) != file_size) + if ((try file_access.read(app, buffer)) != file_size) return error.ScriptLoadFailure; - ev.log(.debug, buffer); + sys.Log.debug.write(app, buffer); } - while (gr.poll()) |_| { - gr.present(); + while (graphics.poll()) |_| { + graphics.present(); } } diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..66c8cc9 --- /dev/null +++ b/src/math.zig @@ -0,0 +1,6 @@ +/// +/// Rounds the `Number` `value` up to the nearest `multiple`. +/// +pub fn roundUp(comptime Number: type, value: Number, multiple: Number) Number { + return value + @mod(@mod(multiple - value, multiple), multiple); +} diff --git a/src/oar.zig b/src/oar.zig new file mode 100644 index 0000000..7660c8c --- /dev/null +++ b/src/oar.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const sys = @import("./sys.zig"); + +/// +/// An entry block of an Oar archive file. +/// +/// Typically, following this block in memory is the file data it holds the meta-information for. +/// +pub const Entry = extern struct { + name_length: u8, + name_buffer: [255]u8 = std.mem.zeroes([255]u8), + file_size: u64, + padding: [248]u8, +}; diff --git a/src/sys.zig b/src/sys.zig index 1de06ec..e0c60b7 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -3,56 +3,29 @@ const ext = @cImport({ }); const io = @import("./io.zig"); +const math = @import("./math.zig"); const mem = @import("./mem.zig"); +const oar = @import("./oar.zig"); const stack = @import("./stack.zig"); const std = @import("std"); -const tar = @import("./tar.zig"); /// /// A thread-safe platform abstraction over multiplexing system I/O processing and event handling. /// pub const App = opaque { /// - /// Linked list of tasks chained together to be processed by the work processor. + /// Linked list of asynchronous messages chained together to be processed by the work processor. /// - const FileSystemMessage = struct { - next: ?*FileSystemMessage = null, + const Message = struct { + next: ?*Message = null, frame: anyframe, - request: union(enum) { - exit, + kind: union(enum) { + quit, - close: struct { - file_access: *FileAccess, - }, - - log: struct { - message: []const u8, - kind: LogKind, - }, - - open: struct { - mode: OpenMode, - file_system_path: *const FileSystem.Path, - result: OpenError!*FileAccess = error.FileNotFound, - }, - - read_file: struct { - file_access: *FileAccess, - buffer: []const u8, - result: FileError!usize = error.Inaccessible, - }, - - seek_file: struct { - file_access: *FileAccess, - origin: SeekOrigin, - offset: usize, - result: FileError!void = error.Inaccessible, - }, - - tell_file: struct { - file_access: *FileAccess, - result: FileError!usize = error.Inaccessible, + task: struct { + data: *anyopaque, + action: fn (*anyopaque) void, }, }, }; @@ -61,13 +34,22 @@ pub const App = opaque { /// Internal state of the event loop hidden from the API consumer. /// const Implementation = struct { - file_system_semaphore: *ext.SDL_sem, - file_system_mutex: *ext.SDL_mutex, - file_system_thread: ?*ext.SDL_Thread, - file_system_messages: ?*FileSystemMessage = null, + data_file_system: FileSystem, + user_file_system: FileSystem, + message_semaphore: *ext.SDL_sem, + message_mutex: *ext.SDL_mutex, + message_thread: ?*ext.SDL_Thread, + messages: ?*Message = null, /// + /// [StartError.OutOfSemaphores] indicates that the process has no more semaphores available + /// to it for use, meaning an [Implementation] may not be initialized at this time. /// + /// [StartError.OutOfMutexes] indicates that the process has no more mutexes available to it + /// for use, meaning an [Implementation] may not be initialized at this time. + /// + /// [StartError.OutOfMemory] indicates that the process has no more memory available to it + /// for use, meaning an [Implementation] may not be initialized at this time. /// const InitError = error { OutOfSemaphores, @@ -76,7 +58,12 @@ pub const App = opaque { }; /// + /// [StartError.OutOfThreads] indicates that the process has no more threads available to it + /// to use, meaning that no asynchronous work may be started on an [Implementation] at this + /// time. /// + /// [StartError.AlreadyStarted] is occurs when a request to start work processing happens on + /// an [Implementation] that is already processing work. /// const StartError = error { OutOfThreads, @@ -84,73 +71,56 @@ pub const App = opaque { }; /// - /// Casts `event_loop` to a [Implementation] reference. + /// Casts `app` to a [Implementation] reference. /// - /// *Note* that if `event_loop` does not have the same alignment as [Implementation], - /// safety-checked undefined behavior will occur. + /// *Note* that if `app` does not have the same alignment as [Implementation], safety- + /// checked undefined behavior will occur. /// - fn cast(event_loop: *EventLoop) *Implementation { - return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), event_loop)); + fn cast(app: *App) *Implementation { + return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), app)); } /// - /// + /// Deinitializes the `implementation`, requesting any running asynchronous workload + /// processes quit and waits for them to do so before freeing any resources. /// fn deinit(implementation: *Implementation) void { - var message = FileSystemMessage{ + var message = Message{ .frame = @frame(), - .request = .exit, + .kind = .quit, }; - implementation.enqueueFileSystemMessage(&message); + @ptrCast(*App, implementation).schedule(&message); { var status = @as(c_int, 0); // SDL2 defines waiting on a null thread reference as a no-op. See // https://wiki.libsdl.org/SDL_WaitThread for more information - ext.SDL_WaitThread(implementation.file_system_thread, &status); + ext.SDL_WaitThread(implementation.message_thread, &status); if (status != 0) { // TODO: Error check this. } } - ext.SDL_DestroyMutex(implementation.file_system_mutex); - ext.SDL_DestroySemaphore(implementation.file_system_semaphore); + ext.SDL_DestroyMutex(implementation.message_mutex); + ext.SDL_DestroySemaphore(implementation.message_semaphore); } /// - /// Enqueues `message` to the file system message processor of `implementation` to be - /// processed at a later, non-deterministic point. + /// Initializes a new [Implemenation] with `data_archive_path` as the read-only data archive + /// to read from and `user_path_prefix` as the native writable user data directory. /// - fn enqueueFileSystemMessage(implementation: *Implementation, - message: *FileSystemMessage) void { - - // TODO: Error check this. - _ = ext.SDL_LockMutex(implementation.file_system_mutex); - - if (implementation.file_system_messages) |messages| { - messages.next = message; - } else { - implementation.file_system_messages = message; - } - - // TODO: Error check these. - _ = ext.SDL_UnlockMutex(implementation.file_system_mutex); - _ = ext.SDL_SemPost(implementation.file_system_semaphore); - } - + /// Returns the created [Implementation] value on success or [InitError] on failure. /// - /// - /// - fn init() InitError!Implementation { + fn init(data_archive_path: []const u8, user_path_prefix: []const u8) InitError!Implementation { return Implementation{ - .file_system_semaphore = ext.SDL_CreateSemaphore(0) - orelse return error.OutOfSemaphores, - - .file_system_mutex = ext.SDL_CreateMutex() orelse return error.OutOfMutexes, - .file_system_thread = null, + .message_semaphore = ext.SDL_CreateSemaphore(0) orelse return error.OutOfSemaphores, + .message_mutex = ext.SDL_CreateMutex() orelse return error.OutOfMutexes, + .data_file_system = .{.archive = data_archive_path}, + .user_file_system = .{.native = user_path_prefix}, + .message_thread = null, }; } @@ -161,57 +131,40 @@ pub const App = opaque { /// The processor returns `0` if it exited normally or any other value if an erroneous exit /// occured. /// - fn processFileSystemMessages(data: ?*anyopaque) callconv(.C) c_int { - const implementation = Implementation.cast(@ptrCast(*EventLoop, data orelse unreachable)); + fn processTasks(userdata: ?*anyopaque) callconv(.C) c_int { + const implementation = Implementation.cast(@ptrCast(*App, userdata orelse unreachable)); while (true) { - while (implementation.file_system_messages) |messages| { - const root = @import("./sys.zig"); + _ = ext.SDL_LockMutex(implementation.message_mutex); - switch (messages.request) { - .exit => return 0, - .log => |*log_request| root.log(log_request.kind, log_request.message), + defer _ = ext.SDL_UnlockMutex(implementation.message_mutex); - .open => |*open_request| open_request.result = - root.open(open_request.mode, open_request.file_system_path.*), - - .close => |*close_request| root.close(close_request.file_access), - - .read_file => |read_request| { - // TODO: Implement. - _ = read_request; - }, - - .seek_file => |seek_request| { - // TODO: Implement. - _ = seek_request; - }, - - .tell_file => |tell_request| { - // TODO: Implement. - _ = tell_request; - }, + while (implementation.messages) |messages| { + switch (messages.kind) { + .quit => return 0, + .task => |task| task.action(task.data), } resume messages.frame; - implementation.file_system_messages = messages.next; + implementation.messages = messages.next; } // TODO: Error check this. - _ = ext.SDL_SemWait(implementation.file_system_semaphore); + _ = ext.SDL_SemWait(implementation.message_semaphore); } } /// + /// Attempts to start the asynchronous worker thread of `implementation` if it hasn't been + /// already. /// + /// [StartError] is returned on failure. /// fn start(implementation: *Implementation) StartError!void { - if (implementation.file_system_thread != null) return error.AlreadyStarted; - - implementation.file_system_thread = ext.SDL_CreateThread(processFileSystemMessages, - "File System Worker", implementation) orelse { + if (implementation.message_thread != null) return error.AlreadyStarted; + implementation.message_thread = ext.SDL_CreateThread(processTasks, "File System Worker", implementation) orelse { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create file-system work processor"); @@ -221,127 +174,39 @@ pub const App = opaque { }; /// - /// Closes access to the file referenced by `file_access` via `event_loop`. + /// Returns a reference to the currently loaded data file-system. /// - /// *Note* that nothing happens to `file_access` if it is already closed. - /// - pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void { - var file_system_message = FileSystemMessage{ - .frame = @frame(), - .request = .{.close = .{.file_access = file_access}}, - }; - - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); + pub fn data(app: *App) *const FileSystem { + return &Implementation.cast(app).data_file_system; } /// - /// Writes `message` to the application log with `kind` via `event_loop`. + /// Enqueues `message` to the message processor of `app` to be processed at a later, non- + /// deterministic point. /// - /// *Note* that `message` is not guaranteed to be partly, wholely, or at all written. - /// - pub fn log(event_loop: *EventLoop, kind: LogKind, message: []const u8) void { - var file_system_message = FileSystemMessage{ - .frame = @frame(), + pub fn schedule(app: *App, message: *Message) void { + const implementation = Implementation.cast(app); - .request = .{.log = .{ - .message = message, - .kind = kind, - }}, - }; + // TODO: Error check these. + _ = ext.SDL_LockMutex(implementation.message_mutex); - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); + defer _ = ext.SDL_UnlockMutex(implementation.message_mutex); + + if (implementation.messages) |messages| { + messages.next = message; + } else { + implementation.messages = message; + } + + // TODO: Error check this. + _ = ext.SDL_SemPost(implementation.message_semaphore); } /// - /// Attempts to open access to a file referenced at `file_system_path` using `mode` as the way - /// to open it via `event_loop`. + /// Returns a reference to the currently loaded user file-system. /// - /// A [FileAccess] pointer is returned referencing the opened file or a [OpenError] if the file - /// could not be opened. - /// - /// *Note* that all files are opened in "binary-mode", or Unix-mode. There are no conversions - /// applied when data is accessed from a file. - /// - pub fn open(event_loop: *EventLoop, mode: OpenMode, - file_system_path: FileSystem.Path) OpenError!*FileAccess { - - var file_system_message = FileSystemMessage{ - .frame = @frame(), - - .request = .{.open = .{ - .mode = mode, - .file_system_path = &file_system_path, - }}, - }; - - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); - - return file_system_message.request.open.result; - } - - /// - /// Attempts to read the contents of the file referenced by `file_access` at the current file - /// cursor position into `buffer`. - /// - /// The number of bytes that could be read / fitted into `buffer` is returned or a [FileError] - /// if the file failed to be read. - /// - pub fn readFile(event_loop: *EventLoop, file_access: *FileAccess, - buffer: []const u8) FileError!usize { - - var file_system_message = FileSystemMessage{ - .frame = @frame(), - - .request = .{.read_file = .{ - .file_access = file_access, - .buffer = buffer, - }}, - }; - - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); - - return file_system_message.request.read_file.result; - } - - /// - /// Attempts to tell the current file cursor position for the file referenced by `file_access`. - /// - /// Returns the number of bytes into the file that the cursor is relative to its beginning or a - /// [FileError] if the file failed to be queried. - /// - pub fn queryFile(event_loop: *EventLoop, file_access: *FileAccess) FileError!usize { - var file_system_message = FileSystemMessage{ - .frame = @frame(), - .request = .{.tell_file = .{.file_access = file_access}}, - }; - - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); - - return file_system_message.request.tell_file.result; - } - - /// - /// Attempts to seek the file cursor through the file referenced by `file_access` from `origin` - /// to `offset` via `event_loop`, returning a [FileError] if the file failed to be sought. - /// - pub fn seekFile(event_loop: *EventLoop, file_access: *FileAccess, - origin: SeekOrigin, offset: usize) FileError!void { - - var file_system_message = FileSystemMessage{ - .frame = @frame(), - - .request = .{ - .seek_file = .{ - .file_access = file_access, - .origin = origin, - .offset = offset, - }, - }, - }; - - suspend Implementation.cast(event_loop).enqueueFileSystemMessage(&file_system_message); - - return file_system_message.request.seek_file.result; + pub fn user(app: *App) *const FileSystem { + return &Implementation.cast(app).user_file_system; } }; @@ -350,49 +215,358 @@ pub const App = opaque { /// pub const FileAccess = opaque { /// - /// Scans the number of bytes in the file referenced by `file_access` via `event_loop`, returing - /// its byte size or a [FileError] if it failed. + /// [Error.FileInaccessible] is a generic catch-all for a [FileAccess] reference no longer + /// pointing to a file or the file becomming invalid for whatever reason. /// - pub fn size(file_access: *FileAccess, event_loop: *EventLoop) FileError!usize { - // Save cursor to return to it later. - const origin_cursor = try event_loop.queryFile(file_access); + pub const Error = error { + FileInaccessible, + }; - try event_loop.seekFile(file_access, .tail, 0); - - const ending_cursor = try event_loop.queryFile(file_access); - - // Return to original cursor. - try event_loop.seekFile(file_access, .head, origin_cursor); - - return ending_cursor; + /// + /// Returns `file_access` casted to a [ext.SDL_RWops]. + /// + fn asRwOps(file_access: *FileAccess) *ext.SDL_RWops { + return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), file_access)); } -}; -/// -/// 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, + /// + /// Close the file referenced by `file_access`, invalidating the reference to it and releasing + /// any associated resources. + /// + /// Freeing an invalid `file_access` has no effect on the file and logs a warning over the + /// wasted effort. + /// + pub fn close(file_access: *FileAccess, app: *App) void { + const Task = struct { + file_access: *FileAccess, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + if (ext.SDL_RWclose(task.file_access.asRwOps()) != 0) + ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, + "Closed an invalid file reference"); + } + }; + + var task = Task{.file_access = file_access}; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + } + + /// + /// Attempts to query the current cursor position for the file referenced by `file_access`. + /// + /// Returns the number of bytes into the file that the cursor is relative to its beginning or a + /// [Error] on failure. + /// + pub fn queryCursor(file_access: *FileAccess, app: *App) Error!u64 { + const Task = struct { + file_access: *FileAccess, + result: Error!u64, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + ext.SDL_ClearError(); + + const sought = ext.SDL_RWtell(task.file_access.asRwOps()); + + if (sought < 0) { + task.result = error.FileInaccessible; + + return; + } + + task.result = @intCast(u64, sought); + } + }; + + var task = Task{ + .file_access = file_access, + .result = error.FileInaccessible, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; + } + + /// + /// Attempts to query the current length for the file referenced by `file_access`. + /// + /// Returns the current length of the file at the time of the operation or a [Error] if the file + /// failed to be queried. + /// + pub fn queryLength(file_access: *FileAccess, app: *App) Error!u64 { + const Task = struct { + file_access: *FileAccess, + result: Error!usize, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + ext.SDL_ClearError(); + + const sought = ext.SDL_RWsize(task.file_access.asRwOps()); + + if (sought < 0) { + task.result = error.FileInaccessible; + + return; + } + + task.result = @intCast(u64, sought); + } + }; + + var task = Task{ + .file_access = file_access, + .result = error.FileInaccessible, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; + } + + /// + /// Attempts to read `file_access` from the its current position into `buffer`, while using + /// `app` as the execution context. + /// + /// Returns the number of bytes that were available to be read, otherwise an [Error] on failure. + /// + pub fn read(file_access: *FileAccess, app: *App, buffer: []u8) Error!usize { + const Task = struct { + file_access: *FileAccess, + buffer: []u8, + result: Error!usize, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + ext.SDL_ClearError(); + + const buffer_read = ext.SDL_RWread(task.file_access.asRwOps(), + task.buffer.ptr, @sizeOf(u8), task.buffer.len); + + if ((buffer_read == 0) and (ext.SDL_GetError() != null)) { + task.result = error.FileInaccessible; + + return; + } + + task.result = buffer_read; + } + }; + + var task = Task{ + .file_access = file_access, + .buffer = buffer, + .result = error.FileInaccessible, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; + } + + /// + /// Attempts to seek `file_access` from the beginning of the file to `cursor` bytes while using + /// `app` as the execution context. + /// + /// Returns [Error] on failure. + /// + pub fn seek(file_access: *FileAccess, app: *App, cursor: u64) Error!void { + const Task = struct { + file_access: *FileAccess, + cursor: u64, + result: Error!void, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + if (task.cursor >= std.math.maxInt(i64)) { + task.result = error.OutOfRange; + + return; + } + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(task.file_access.asRwOps(), + @intCast(i64, task.cursor), ext.RW_SEEK_SET) < 0) { + + task.result = error.FileInaccessible; + + return; + } + + task.result = {}; + } + }; + + var task = Task{ + .file_access = file_access, + .cursor = cursor, + .result = error.FileInaccessible, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; + } + + /// + /// Attempts to seek `file_access` to the end of the file while using `app` as the execution + /// context. + /// + /// Returns [Error] on failure. + /// + pub fn seekToEnd(file_access: *FileAccess, app: *App) Error!void { + const Task = struct { + file_access: *FileAccess, + result: Error!void, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(task.file_access.asRwOps(), 0, ext.RW_SEEK_END) < 0) { + task.result = error.FileInaccessible; + + return; + } + + task.result = {}; + } + }; + + var task = Task{ + .file_access = file_access, + .result = error.FileInaccessible, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; + } }; /// /// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s) /// available to the application in a sandboxed environment. /// -pub const FileSystem = struct { - data: Root, - user: Root, +pub const FileSystem = union(enum) { + native: []const u8, + archive: []const u8, /// /// Platform-agnostic mechanism for referencing files and directories on a [FileSystem]. /// pub const Path = struct { - root: *const Root, - length: u16, + file_system: *const FileSystem, + length: u8, buffer: [max]u8, + /// + /// 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 [OpenError.FileNotFound] errors. + /// + /// When a given [FileSystem] does not support a specified [OpenMode], + /// [OpenError.ModeUnsupported] is used to inform the consuming code that another [OpenMode] + /// should be tried or, if no mode other is suitable, that the resource is effectively + /// unavailable. + /// + pub const OpenError = error { + FileNotFound, + ModeUnsupported, + }; + + /// + /// [OpenMode.readonly] indicates that an existing file is opened in a read-only state, + /// disallowing write access. + /// + /// [OpenMode.overwrite] indicates that an empty file has been created or an existing file + /// has been completely overwritten into. + /// + /// [OpenMode.append] indicates that an existing file that has been opened for reading from + /// and writing to on the end of existing data. + /// + pub const OpenMode = enum { + readonly, + overwrite, + append, + }; + /// /// Returns `true` if the length of `path` is empty, otherwise `false`. /// @@ -404,7 +578,8 @@ pub const FileSystem = struct { /// Returns `true` if `this` is equal to `that`, otherwise `false`. /// pub fn equals(this: Path, that: Path) bool { - return std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]); + return (this.file_system == that.file_system) and + std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]); } /// @@ -414,13 +589,171 @@ pub const FileSystem = struct { /// byte. Because of this, it is not safe to asume that a path may hold [max] individual /// characters. /// - pub const max = 512; + pub const max = 255; /// + /// Attempts to open the file identified by `path` with `mode` as the mode for opening the + /// file and `app` as the execution context. /// + /// Returns a [FileAccess] reference that provides access to the file referenced by `path` + /// or a [OpenError] if it failed. /// - pub fn write(path: Path, writer: io.Writer) bool { - return (writer.write(path.buffer[0 .. path.length]) == path.length); + pub fn open(path: Path, app: *App, mode: OpenMode) OpenError!*FileAccess { + const Task = struct { + path: *const FileSystem.Path, + app: *App, + mode: OpenMode, + result: OpenError!*FileAccess, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + switch (task.path.file_system.*) { + .archive => |archive| { + if (archive.len == 0) { + task.result = error.FileNotFound; + + return; + } + + if (task.mode != .readonly) { + task.result = error.ModeUnsupported; + + return; + } + + var path_buffer = std.mem.zeroes([4096]u8); + + if (archive.len >= path_buffer.len) { + task.result = error.FileNotFound; + + return; + } + + std.mem.copy(u8, path_buffer[0 ..], archive); + + ext.SDL_ClearError(); + + const rw_ops = ext.SDL_RWFromFile(&path_buffer, "rb") orelse { + task.result = error.FileNotFound; + + return; + }; + + while (true) { + var entry = std.mem.zeroes(oar.Entry); + const entry_buffer = std.mem.asBytes(&entry); + + ext.SDL_ClearError(); + + if (ext.SDL_RWread(rw_ops, entry_buffer, @sizeOf(u8), + entry_buffer.len) != entry_buffer.len) { + + task.result = error.FileNotFound; + + return; + } + + if (std.mem.eql(u8, entry.name_buffer[0 .. entry.name_length], + task.path.buffer[0 .. task.path.length])) { + + task.result = @ptrCast(*FileAccess, rw_ops); + + return; + } + + { + var to_read = math.roundUp(u64, + entry.file_size, entry_buffer.len); + + while (to_read != 0) { + const read = @intCast(i64, std.math.min( + to_read, std.math.maxInt(i64))); + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(rw_ops, read, ext.RW_SEEK_CUR) < 0) { + task.result = error.FileNotFound; + + return; + } + + // Cannot be less than zero because it is derived from + // `read`. + to_read -= @intCast(u64, read); + } + } + } + }, + + .native => |native| { + if (native.len == 0) { + task.result = error.FileNotFound; + + return; + } + + var path_buffer = std.mem.zeroes([4096]u8); + const seperator = '/'; + + const seperator_length = + @boolToInt(native[native.len - 1] != seperator); + + if ((native.len + seperator_length + + task.path.length) >= path_buffer.len) { + + task.result = error.FileNotFound; + + return; + } + + std.mem.copy(u8, path_buffer[0 ..], native); + + if (seperator_length != 0) + path_buffer[native.len] = seperator; + + std.mem.copy(u8, path_buffer[native.len .. path_buffer.len], + task.path.buffer[0 .. task.path.length]); + + ext.SDL_ClearError(); + + task.result = @ptrCast(*FileAccess, ext.SDL_RWFromFile( + &path_buffer, switch (task.mode) { + .readonly => "rb", + .overwrite => "wb", + .append => "ab", + }) orelse { + + task.result = error.FileNotFound; + + return; + }); + }, + } + } + }; + + var task = Task{ + .mode = mode, + .path = &path, + .app = app, + .result = error.FileNotFound, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }}, + }; + + suspend app.schedule(&message); + + return task.result; } }; @@ -433,59 +766,57 @@ pub const FileSystem = struct { }; /// + /// Attempts to create a [Path] with `file_system` as the file-system root and the path + /// components in `sequences` as a fully qualified path from the root. /// + /// A [Path] value is returned containing the fully qualified path from the file-system root or + /// a [PathError] if it could not be created. /// - pub const Root = union(enum) { - native: []const u8, - archive: *tar.Archive, + pub fn joinedPath(file_system: *const FileSystem, + sequences: []const []const u8) PathError!Path { - /// - /// - /// - pub fn joinedPath(root: *const Root, sequences: []const []const u8) PathError!Path { - var path = Path{ - .root = root, - .buffer = std.mem.zeroes([Path.max]u8), - .length = 0, - }; + var path = Path{ + .file_system = file_system, + .buffer = std.mem.zeroes([Path.max]u8), + .length = 0, + }; - if (sequences.len != 0) { - const last_sequence_index = sequences.len - 1; + if (sequences.len != 0) { + const last_sequence_index = sequences.len - 1; - for (sequences) |sequence, index| if (sequence.len != 0) { - var components = mem.Spliterator(u8){ - .source = sequence, - .delimiter = "/", - }; + for (sequences) |sequence, index| if (sequence.len != 0) { + var components = mem.Spliterator(u8){ + .source = sequence, + .delimiter = "/", + }; - while (components.next()) |component| if (component.len != 0) { - for (component) |byte| { - if (path.length == Path.max) return error.TooLong; + while (components.next()) |component| if (component.len != 0) { + for (component) |byte| { + if (path.length == Path.max) return error.TooLong; - path.buffer[path.length] = byte; - path.length += 1; - } + path.buffer[path.length] = byte; + path.length += 1; + } - if (components.hasNext()) { - if (path.length == Path.max) return error.TooLong; - - path.buffer[path.length] = '/'; - path.length += 1; - } - }; - - if (index < last_sequence_index) { + if (components.hasNext()) { if (path.length == Path.max) return error.TooLong; path.buffer[path.length] = '/'; path.length += 1; } }; - } - return path; + if (index < last_sequence_index) { + if (path.length == Path.max) return error.TooLong; + + path.buffer[path.length] = '/'; + path.length += 1; + } + }; } - }; + + return path; + } }; /// @@ -503,6 +834,9 @@ pub const GraphicsContext = opaque { const Keys = [256]bool; }; + /// + /// + /// const Implementation = struct { event: Event, }; @@ -526,55 +860,68 @@ pub const GraphicsContext = opaque { }; /// -/// +/// Returns a graphics runner that uses `Errors` as its error set. /// pub fn GraphicsRunner(comptime Errors: type) type { - return fn (*EventLoop, *FileSystem, *GraphicsContext) callconv(.Async) Errors!void; + return fn (*App, *GraphicsContext) callconv(.Async) Errors!void; } /// -/// [LogKind.info] represents a log message which is purely informative and does not indicate -/// any kind of issue. +/// [Log.info] represents a log message which is purely informative and does not indicate any kind +/// of issue. /// -/// [LogKind.debug] represents a log message which is purely for debugging purposes and will -/// only occurs in debug builds. +/// [Log.debug] represents a log message which is purely for debugging purposes and will only occurs +/// in debug builds. /// -/// [LogKind.warning] represents a log message which is a warning about a issue that does not -/// break anything important but is not ideal. +/// [Log.warning] represents a log message which is a warning about a issue that does not break +/// anything important but is not ideal. /// -pub const LogKind = enum(u32) { +pub const Log = enum(u32) { info = ext.SDL_LOG_PRIORITY_INFO, debug = ext.SDL_LOG_PRIORITY_DEBUG, warning = ext.SDL_LOG_PRIORITY_WARN, + + /// + /// Writes `utf8_message` as the log kind identified by `log` with `app` as the execution + /// context. + /// + pub fn write(log: Log, app: *App, utf8_message: []const u8) void { + const Task = struct { + log: Log, + utf8_message: []const u8, + + const Task = @This(); + + fn process(data: *anyopaque) void { + const task = @ptrCast(*Task, @alignCast(@alignOf(Task), data)); + + ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION, @enumToInt(task.log), + "%.*s", task.utf8_message.len, task.utf8_message.ptr); + } + }; + + var task = Task{ + .log = log, + .utf8_message = utf8_message, + }; + + var message = App.Message{ + .frame = @frame(), + + .kind = .{.task = .{ + .data = &task, + .action = Task.process, + }} + }; + + suspend app.schedule(&message); + } }; /// -/// [OpenError.FileNotFound] is a catch-all for when a file could not be located to be opened. This -/// may be as simple as it doesn't exist or the because the underlying file-system will not / -/// cannot give access to it at this time. -/// -pub const OpenError = error { - FileNotFound, -}; - -/// -/// [OpenMode.readonly] indicates that an existing file is opened in a read-only state, -/// disallowing write access. -/// -/// [OpenMode.overwrite] indicates that an empty file has been created or an existing file has -/// been completely overwritten into. -/// -/// [OpenMode.append] indicates that an existing file that has been opened for reading from and -/// writing to on the end of existing data. -/// -pub const OpenMode = enum { - readonly, - overwrite, - append, -}; - -/// +/// [RunError.InitFailure] occurs if a necessary resource fails to be acquired or allocated. /// +/// [RunError.AlreadyRunning] occurs if a runner has already been started. /// pub const RunError = error { InitFailure, @@ -582,75 +929,14 @@ pub const RunError = error { }; /// -/// [SeekOrigin.head] indicates that a seek operation will seek from the offset origin of the -/// file beginning, or "head". +/// Runs a graphical application referenced by `run` with `error` as its error set and `allocator` +/// as the underlying memory allocation strategy for its graphical runtime. /// -/// [SeekOrigin.tail] indicates that a seek operation will seek from the offset origin of the -/// file end, or "tail". +/// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if +/// the underlying runtime fails and is logged. /// -/// [SeekOrigin.cursor] indicates that a seek operation will seek from the current position of -/// the file cursor. -/// -pub const SeekOrigin = enum { - head, - tail, - cursor, -}; - -/// -/// -/// -pub fn close(file_access: *FileAccess) void { - if (ext.SDL_RWclose(@ptrCast(*ext.SDL_RWops, - @alignCast(@alignOf(ext.SDL_RWops), file_access))) != 0) { - - ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, - "Failed to close file - may have been already closed"); - } -} - -/// -/// -/// -pub fn log(kind: LogKind, message: []const u8) void { - ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION, - @enumToInt(kind), "%.*s", message.len, message.ptr); -} - -/// -/// -/// -pub fn open(mode: OpenMode, file_system_path: FileSystem.Path) OpenError!*FileAccess { - switch (file_system_path.root.*) { - .archive => |archive| { - // TODO: Implement. - _ = archive; - - return error.FileNotFound; - }, - - .native => |native| { - var path_buffer = std.mem.zeroes([4096]u8); - var path = stack.Fixed(u8){.buffer = path_buffer[0 .. ]}; - - path.pushAll(native) catch return error.FileNotFound; - - if (file_system_path.write(path.writer())) return error.FileNotFound; - - return @ptrCast(?*FileAccess, ext.SDL_RWFromFile(&path_buffer, switch (mode) { - .readonly => "rb", - .overwrite => "wb", - .append => "ab", - })) orelse error.FileNotFound; - }, - } -} - -/// -/// -/// -pub fn runGraphics(comptime Errors: anytype, allocator: std.mem.Allocator, - comptime run: GraphicsRunner(Errors)) (RunError || Errors)!void { +pub fn runGraphics(comptime Error: anytype, + comptime run: GraphicsRunner(Error)) (RunError || Error)!void { if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime"); @@ -685,29 +971,18 @@ pub fn runGraphics(comptime Errors: anytype, allocator: std.mem.Allocator, defer ext.SDL_DestroyRenderer(renderer); - const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse { + const user_path_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load user path"); return error.InitFailure; }; - defer ext.SDL_free(user_prefix); + defer ext.SDL_free(user_path_prefix); - var data_archive = tar.Archive.init(allocator); + var app = App.Implementation.init("./data.oar", user_path_prefix + [0 .. std.mem.len(user_path_prefix)]) catch |err| { - data_archive.load("./data.tar") catch |err| switch (err) { - error.FileNotFound => { - - }, - }; - - var file_system = FileSystem{ - .user = .{.native = user_prefix[0 .. std.mem.len(user_prefix)]}, - .data = .{.archive = &data_archive}, - }; - - var event_loop = EventLoop.Implementation.init() catch |err| { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, switch (err) { error.OutOfMemory => "Failed to allocate necessary memory", error.OutOfMutexes => "Failed to create file-system work lock", @@ -717,16 +992,14 @@ pub fn runGraphics(comptime Errors: anytype, allocator: std.mem.Allocator, return error.InitFailure; }; - defer event_loop.deinit(); + defer app.deinit(); - event_loop.start() catch |err| { - switch (err) { + app.start() catch |err| { + ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, switch (err) { // Not possible for it to have already been started. error.AlreadyStarted => unreachable, - - error.OutOfThreads => ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, - "Failed to start file-system work processor"), - } + error.OutOfThreads => "Failed to start file-system work processor", + }); return error.InitFailure; }; @@ -737,6 +1010,5 @@ pub fn runGraphics(comptime Errors: anytype, allocator: std.mem.Allocator, }, }; - return run(@ptrCast(*EventLoop, &event_loop), &file_system, - @ptrCast(*GraphicsContext, &graphics_context)); + return run(@ptrCast(*App, &app), @ptrCast(*GraphicsContext, &graphics_context)); } diff --git a/src/tar.zig b/src/tar.zig deleted file mode 100644 index 7adb58e..0000000 --- a/src/tar.zig +++ /dev/null @@ -1,29 +0,0 @@ -const std = @import("std"); - -/// -/// -/// -pub const Archive = struct { - allocator: std.mem.Allocator, - - pub const LoadError = error { - FileNotFound, - }; - - /// - /// - /// - pub fn init(allocator: std.mem.Allocator) Archive { - return Archive{ - .allocator = allocator, - }; - } - - /// - /// - /// - pub fn load(archive: *Archive, file_path: []const u8) LoadError!void { - _ = file_path; - _ = archive; - } -};