diff --git a/src/main.zig b/src/main.zig index 05ac72f..2f85626 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,7 +27,7 @@ fn run(app: *sys.AppContext, graphics: *sys.GraphicsContext) anyerror!void { defer _ = gpa.deinit(); { - const file_access = try (try app.data().joinedPath(&.{"ona.lua"})).open(.readonly); + var file_access = try (try app.data().joinedPath(&.{"ona.lua"})).open(.readonly); defer file_access.close(); diff --git a/src/oar.zig b/src/oar.zig index be4f3d4..6a72116 100644 --- a/src/oar.zig +++ b/src/oar.zig @@ -9,36 +9,34 @@ const sys = @import("./sys.zig"); pub const Entry = extern struct { signature: [3]u8 = signature_magic, revision: u8, - name_length: u8 = 0, name_buffer: [255]u8 = std.mem.zeroes([255]u8), + name_length: u8 = 0, file_size: u64, - padding: [244]u8 = std.mem.zeroes([244]u8), + file_offset: u64, + padding: [232]u8 = std.mem.zeroes([232]u8), - /// - /// Returns `true` if `entry` correctly identifies itself as a valid Oar entry, otherwise - /// `false`. - /// - pub fn isValid(entry: Entry) bool { - return std.mem.eql(u8, &entry.signature, signature_magic[0 ..]); + comptime { + const entry_size = @sizeOf(Entry); + + if (entry_size != 512) + @compileError("Entry is " ++ std.fmt.comptimePrint("{d}", .{entry_size}) ++ " bytes"); } /// - /// Attempts to read an [Entry] from `file_access` at its current cursor position. /// - /// Returns the read [Entry] value or `null` if the end of file is reached before completing the - /// read. /// - pub fn read(file_access: *sys.FileAccess) sys.FileAccess.Error!?Entry { - var entry = std.mem.zeroes(Entry); - const origin = try file_access.queryCursor(); + pub const FindError = sys.FileAccess.Error || error { + EntryNotFound, + }; - if ((try file_access.read(std.mem.asBytes(&entry))) != @sizeOf(Entry)) { - try file_access.seek(origin); + /// + /// + /// + pub fn find(file_access: sys.FileAccess, entry_name: []const u8) FindError!Entry { + _ = file_access; + _ = entry_name; - return null; - } - - return entry; + return error.EntryNotFound; } /// diff --git a/src/sys.zig b/src/sys.zig index 6d6fb4f..4bb595e 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -128,19 +128,19 @@ pub const AppContext = opaque { } /// - /// 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. + /// Initializes a new [Implemenation] with `data_archive_file_access` as the data archive to + /// read from and `user_path_prefix` as the native writable user data directory. /// /// Returns the created [Implementation] value on success or [InitError] on failure. /// - fn init(data_archive_path: []const u8, + fn init(data_archive_file_access: FileAccess, user_path_prefix: []const u8) InitError!Implementation { return Implementation{ .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}, + .data_file_system = .{.archive = .{.file_access = data_archive_file_access}}, + .user_file_system = .{.native = .{.path_prefix = user_path_prefix}}, .message_thread = null, }; } @@ -204,7 +204,7 @@ pub const AppContext = opaque { /// /// Returns a reference to the currently loaded data file-system. /// - pub fn data(app_context: *AppContext) *const FileSystem { + pub fn data(app_context: *AppContext) *FileSystem { return &Implementation.cast(app_context).data_file_system; } @@ -245,7 +245,7 @@ pub const AppContext = opaque { /// /// Returns a reference to the currently loaded user file-system. /// - pub fn user(app_context: *AppContext) *const FileSystem { + pub fn user(app_context: *AppContext) *FileSystem { return &Implementation.cast(app_context).user_file_system; } }; @@ -253,7 +253,22 @@ pub const AppContext = opaque { /// /// File-system agnostic abstraction for manipulating a file. /// -pub const FileAccess = opaque { +pub const FileAccess = struct { + context: *anyopaque, + implementation: *const Implementation, + + /// + /// Provides a set of implementation-specific behaviors to a [FileAccess] instance. + /// + pub const Implementation = struct { + close: fn (*anyopaque) void, + queryCursor: fn (*anyopaque) Error!u64, + queryLength: fn (*anyopaque) Error!u64, + read: fn (*anyopaque, []u8) Error!usize, + seek: fn (*anyopaque, u64) Error!void, + seekToEnd: fn (*anyopaque) Error!void, + }; + /// /// [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. @@ -262,13 +277,6 @@ pub const FileAccess = opaque { FileInaccessible, }; - /// - /// 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)); - } - /// /// Close the file referenced by `file_access` on the main thread, invalidating the reference to /// it and releasing any associated resources. @@ -277,8 +285,7 @@ pub const FileAccess = opaque { /// wasted effort. /// pub fn close(file_access: *FileAccess) void { - if (ext.SDL_RWclose(file_access.asRwOps()) != 0) - ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, "Closed an invalid file reference"); + return file_access.implementation.close(file_access.context); } /// @@ -288,13 +295,7 @@ pub const FileAccess = opaque { /// [Error] on failure. /// pub fn queryCursor(file_access: *FileAccess) Error!u64 { - ext.SDL_ClearError(); - - const sought = ext.SDL_RWtell(file_access.asRwOps()); - - if (sought < 0) return error.FileInaccessible; - - return @intCast(u64, sought); + return file_access.implementation.queryCursor(file_access.context); } /// @@ -304,13 +305,7 @@ pub const FileAccess = opaque { /// failed to be queried. /// pub fn queryLength(file_access: *FileAccess) Error!u64 { - ext.SDL_ClearError(); - - const sought = ext.SDL_RWsize(file_access.asRwOps()); - - if (sought < 0) return error.FileInaccessible; - - return @intCast(u64, sought); + return file_access.implementation.queryLength(file_access.context); } /// @@ -319,14 +314,7 @@ pub const FileAccess = opaque { /// Returns the number of bytes that were available to be read, otherwise an [Error] on failure. /// pub fn read(file_access: *FileAccess, buffer: []u8) Error!usize { - ext.SDL_ClearError(); - - const buffer_read = - ext.SDL_RWread(file_access.asRwOps(), buffer.ptr, @sizeOf(u8), buffer.len); - - if ((buffer_read == 0) and (ext.SDL_GetError() != null)) return error.FileInaccessible; - - return buffer_read; + return file_access.implementation.read(file_access.context, buffer); } /// @@ -335,19 +323,7 @@ pub const FileAccess = opaque { /// Returns [Error] on failure. /// pub fn seek(file_access: *FileAccess, cursor: u64) Error!void { - var to_seek = cursor; - - while (to_seek != 0) { - const sought = @intCast(i64, std.math.min(to_seek, std.math.maxInt(i64))); - - ext.SDL_ClearError(); - - if (ext.SDL_RWseek(file_access.asRwOps(), sought, ext.RW_SEEK_CUR) < 0) - return error.FileInaccessible; - - // Cannot be less than zero because it is derived from `read`. - to_seek -= @intCast(u64, sought); - } + return file_access.implementation.seek(file_access.context, cursor); } /// @@ -357,10 +333,7 @@ pub const FileAccess = opaque { /// Returns [Error] on failure. /// pub fn seekToEnd(file_access: *FileAccess) Error!void { - ext.SDL_ClearError(); - - if (ext.SDL_RWseek(file_access.asRwOps(), 0, ext.RW_SEEK_END) < 0) - return error.FileInaccessible; + return file_access.implementation.seekToEnd(file_access.context); } }; @@ -369,14 +342,33 @@ pub const FileAccess = opaque { /// available to the application in a sandboxed environment. /// pub const FileSystem = union(enum) { - native: []const u8, - archive: []const u8, + native: struct { + path_prefix: []const u8, + }, + + archive: struct { + file_access: FileAccess, + + entry_table: [max_open_entries]ArchiveEntry = + std.mem.zeroes([max_open_entries]ArchiveEntry), + + const max_open_entries = 16; + }, + + /// + /// Handles the state of an opened archive entry. + /// + const ArchiveEntry = struct { + using: ?*FileAccess, + header: oar.Entry, + cursor: u64, + }; /// /// Platform-agnostic mechanism for referencing files and directories on a [FileSystem]. /// pub const Path = struct { - file_system: *const FileSystem, + file_system: *FileSystem, length: u8, buffer: [max]u8, @@ -390,9 +382,13 @@ pub const FileSystem = union(enum) { /// should be tried or, if no mode other is suitable, that the resource is effectively /// unavailable. /// + /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] + /// is used to communicate this. + /// pub const OpenError = error { FileNotFound, ModeUnsupported, + OutOfFiles, }; /// @@ -442,59 +438,205 @@ pub const FileSystem = union(enum) { /// Returns a [FileAccess] reference that provides access to the file referenced by `path` /// or a [OpenError] if it failed. /// - pub fn open(path: Path, mode: OpenMode) OpenError!*FileAccess { + pub fn open(path: Path, mode: OpenMode) OpenError!FileAccess { switch (path.file_system.*) { - .archive => |archive| { - if (archive.len == 0) return error.FileNotFound; - + .archive => |*archive| { if (mode != .readonly) return error.ModeUnsupported; - var path_buffer = std.mem.zeroes([4096]u8); + for (archive.entry_table) |_, index| { + if (archive.entry_table[index].using == null) { + archive.entry_table[index] = .{ + .header = oar.Entry.find(archive.file_access, path. + buffer[0 .. path.length]) catch return error.FileNotFound, - if (archive.len >= path_buffer.len) return error.FileNotFound; + .using = &archive.file_access, + .cursor = 0, + }; - std.mem.copy(u8, path_buffer[0 ..], archive); + const Implementation = struct { + fn archiveEntryCast(context: *anyopaque) *ArchiveEntry { + return @ptrCast(*ArchiveEntry, @alignCast( + @alignOf(ArchiveEntry), context)); + } - const file_access = @ptrCast(*FileAccess, ext.SDL_RWFromFile( - &path_buffer, "rb") orelse return error.FileNotFound); + fn close(context: *anyopaque) void { + archiveEntryCast(context).using = null; + } - while (oar.Entry.read(file_access) catch return error.FileNotFound) |entry| { - if (!entry.isValid()) break; + fn queryCursor(context: *anyopaque) FileAccess.Error!u64 { + const archive_entry = archiveEntryCast(context); - if (std.mem.eql(u8, entry.name_buffer[0 .. entry.name_length], - path.buffer[0 .. path.length])) return file_access; + if (archive_entry.using == null) return error.FileInaccessible; - file_access.seek(entry.file_size) catch break; + return archive_entry.cursor; + } + + fn queryLength(context: *anyopaque) FileAccess.Error!u64 { + const archive_entry = archiveEntryCast(context); + + if (archive_entry.using == null) return error.FileInaccessible; + + return archive_entry.header.file_size; + } + + fn read(context: *anyopaque, buffer: []u8) FileAccess.Error!usize { + const archive_entry = archiveEntryCast(context); + + const file_access = archive_entry.using orelse + return error.FileInaccessible; + + if (archive_entry.cursor >= archive_entry.header.file_size) + return error.FileInaccessible; + + try file_access.seek(archive_entry.header.file_offset); + + return file_access.read(buffer[0 .. std.math.min( + buffer.len, archive_entry.header.file_size)]); + } + + fn seek(context: *anyopaque, cursor: usize) FileAccess.Error!void { + const archive_entry = archiveEntryCast(context); + + if (archive_entry.using == null) return error.FileInaccessible; + + archive_entry.cursor = cursor; + } + + fn seekToEnd(context: *anyopaque) FileAccess.Error!void { + const archive_entry = archiveEntryCast(context); + + if (archive_entry.using == null) return error.FileInaccessible; + + archive_entry.cursor = archive_entry.header.file_size; + } + }; + + return FileAccess{ + .context = &archive.entry_table[index], + + .implementation = &.{ + .close = Implementation.close, + .queryCursor = Implementation.queryCursor, + .queryLength = Implementation.queryLength, + .read = Implementation.read, + .seek = Implementation.seek, + .seekToEnd = Implementation.seekToEnd, + }, + }; + } } - return error.FileNotFound; + return error.OutOfFiles; }, .native => |native| { - if (native.len == 0) return error.FileNotFound; + if (native.path_prefix.len == 0) return error.FileNotFound; var path_buffer = std.mem.zeroes([4096]u8); const seperator = '/'; - const seperator_length = @boolToInt(native[native.len - 1] != seperator); - if ((native.len + seperator_length + path.length) >= path_buffer.len) - return error.FileNotFound; + const seperator_length = @boolToInt(native.path_prefix[ + native.path_prefix.len - 1] != seperator); - std.mem.copy(u8, path_buffer[0 ..], native); + if ((native.path_prefix.len + seperator_length + path.length) >= + path_buffer.len) return error.FileNotFound; + + std.mem.copy(u8, path_buffer[0 ..], native.path_prefix); if (seperator_length != 0) - path_buffer[native.len] = seperator; + path_buffer[native.path_prefix.len] = seperator; - std.mem.copy(u8, path_buffer[native.len .. path_buffer.len], - path.buffer[0 .. path.length]); + std.mem.copy(u8, path_buffer[native.path_prefix.len .. + path_buffer.len], path.buffer[0 .. path.length]); ext.SDL_ClearError(); - return @ptrCast(*FileAccess, ext.SDL_RWFromFile(&path_buffer, switch (mode) { - .readonly => "rb", - .overwrite => "wb", - .append => "ab", - }) orelse return error.FileNotFound); + const Implementation = struct { + fn rwOpsCast(context: *anyopaque) *ext.SDL_RWops { + return @ptrCast(*ext.SDL_RWops, @alignCast( + @alignOf(ext.SDL_RWops), context)); + } + + fn close(context: *anyopaque) void { + ext.SDL_ClearError(); + + if (ext.SDL_RWclose(rwOpsCast(context)) != 0) + ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, ext.SDL_GetError()); + } + + fn queryCursor(context: *anyopaque) FileAccess.Error!u64 { + ext.SDL_ClearError(); + + const sought = ext.SDL_RWtell(rwOpsCast(context)); + + if (sought < 0) return error.FileInaccessible; + + return @intCast(u64, sought); + } + + fn queryLength(context: *anyopaque) FileAccess.Error!u64 { + ext.SDL_ClearError(); + + const sought = ext.SDL_RWsize(rwOpsCast(context)); + + if (sought < 0) return error.FileInaccessible; + + return @intCast(u64, sought); + } + + fn read(context: *anyopaque, buffer: []u8) FileAccess.Error!usize { + ext.SDL_ClearError(); + + const buffer_read = ext.SDL_RWread(rwOpsCast( + context), buffer.ptr, @sizeOf(u8), buffer.len); + + if ((buffer_read == 0) and (ext.SDL_GetError() != null)) + return error.FileInaccessible; + + return buffer_read; + } + + fn seek(context: *anyopaque, cursor: usize) FileAccess.Error!void { + var to_seek = cursor; + + while (to_seek != 0) { + const math = std.math; + const sought = @intCast(i64, math.min(to_seek, math.maxInt(i64))); + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(rwOpsCast(context), sought, ext.RW_SEEK_CUR) < 0) + return error.FileInaccessible; + + // Cannot be less than zero because it is derived from `read`. + to_seek -= @intCast(u64, sought); + } + } + + fn seekToEnd(context: *anyopaque) FileAccess.Error!void { + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(rwOpsCast(context), 0, ext.RW_SEEK_END) < 0) + return error.FileInaccessible; + } + }; + + return FileAccess{ + .context = ext.SDL_RWFromFile(&path_buffer, switch (mode) { + .readonly => "rb", + .overwrite => "wb", + .append => "ab", + }) orelse return error.FileNotFound, + + .implementation = &.{ + .close = Implementation.close, + .queryCursor = Implementation.queryCursor, + .queryLength = Implementation.queryLength, + .read = Implementation.read, + .seek = Implementation.seek, + .seekToEnd = Implementation.seekToEnd, + }, + }; }, } } @@ -515,9 +657,7 @@ pub const FileSystem = union(enum) { /// 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 fn joinedPath(file_system: *const FileSystem, - sequences: []const []const u8) PathError!Path { - + pub fn joinedPath(file_system: *FileSystem, sequences: []const []const u8) PathError!Path { var path = Path{ .file_system = file_system, .buffer = std.mem.zeroes([Path.max]u8), @@ -644,8 +784,7 @@ pub const RunError = error { }; /// -/// 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. +/// Runs a graphical application referenced by `run` with `error` as its error set. /// /// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if /// the underlying runtime fails and is logged. @@ -694,8 +833,15 @@ pub fn runGraphics(comptime Error: anytype, defer ext.SDL_free(user_path_prefix); - var app_context = AppContext.Implementation.init("./data.oar", user_path_prefix - [0 .. std.mem.len(user_path_prefix)]) catch |err| { + var cwd_file_system = FileSystem{.native =.{.path_prefix = "./"}}; + + var data_archive_file_access = try (try cwd_file_system. + joinedPath(&.{"./data.oar"})).open(.readonly); + + defer data_archive_file_access.close(); + + var app_context = AppContext.Implementation.init(data_archive_file_access, + user_path_prefix[0 .. std.mem.len(user_path_prefix)]) catch |err| { ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, switch (err) { error.OutOfMemory => "Failed to allocate necessary memory",