diff --git a/src/engine/main.zig b/src/engine/main.zig index 1dd0322..3202d11 100644 --- a/src/engine/main.zig +++ b/src/engine/main.zig @@ -15,7 +15,7 @@ fn run(app: *sys.AppContext, graphics: *sys.GraphicsContext) anyerror!void { defer _ = gpa.deinit(); { - var file_access = try (try app.data().joinedPath(&.{"ona.lua"})).open(.readonly); + var file_access = try app.data().open(try sys.Path.joined(&.{"ona.lua"}), .readonly); defer file_access.close(); diff --git a/src/engine/sys.zig b/src/engine/sys.zig index 63a01c5..0c628e2 100644 --- a/src/engine/sys.zig +++ b/src/engine/sys.zig @@ -274,305 +274,160 @@ pub const FileSystem = union(enum) { archive: Archive, /// - /// Platform-agnostic mechanism for referencing files and directories on a [FileSystem]. + /// 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. /// - pub const Path = struct { - file_system: *FileSystem, - path: oar.Path, + /// 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. + /// + /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] + /// is used to communicate this. + /// + pub const OpenError = error { + FileNotFound, + ModeUnsupported, + OutOfFiles, + }; - /// - /// 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. - /// - /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] - /// is used to communicate this. - /// - pub const OpenError = error { - FileNotFound, - ModeUnsupported, - OutOfFiles, - }; + /// + /// [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, + }; - /// - /// [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, - }; + /// + /// Attempts to open the file identified by `path` with `mode` as the mode for opening the file. + /// + /// Returns a [FileAccess] reference that provides access to the file referenced by `path`or a + /// [OpenError] if it failed. + /// + pub fn open(file_system: *FileSystem, path: Path, mode: OpenMode) OpenError!ona.io.FileAccess { + switch (file_system.*) { + .archive => |*archive| { + if (mode != .readonly) return error.ModeUnsupported; - /// - /// Returns `true` if the length of `path` is empty, otherwise `false`. - /// - pub fn isEmpty(path: Path) bool { - return (path.length == 0); - } - - /// - /// Returns `true` if `this` is equal to `that`, otherwise `false`. - /// - pub fn equals(this: Path, that: Path) bool { - return (this.file_system == that.file_system) and - std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]); - } - - /// - /// 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 = 255; - - /// - /// Attempts to open the file identified by `path` with `mode` as the mode for opening the - /// file. - /// - /// 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!ona.io.FileAccess { - switch (path.file_system.*) { - .archive => |*archive| { - if (mode != .readonly) return error.ModeUnsupported; - - const FileAccess = ona.io.FileAccess; - - for (archive.entry_table) |*entry| if (entry.owner == null) { - const Implementation = struct { - fn archiveEntryCast(context: *anyopaque) *Archive.Entry { - return @ptrCast(*Archive.Entry, @alignCast( - @alignOf(Archive.Entry), context)); - } - - fn close(context: *anyopaque) void { - archiveEntryCast(context).owner = null; - } - - fn queryCursor(context: *anyopaque) FileAccess.Error!u64 { - const archive_entry = archiveEntryCast(context); - - if (archive_entry.owner == null) return error.FileInaccessible; - - return archive_entry.cursor; - } - - fn queryLength(context: *anyopaque) FileAccess.Error!u64 { - const archive_entry = archiveEntryCast(context); - - if (archive_entry.owner == 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.owner orelse - return error.FileInaccessible; - - if (archive_entry.cursor >= archive_entry.header.file_size) - return error.FileInaccessible; - - try file_access.seek(archive_entry.header.absolute_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.owner == null) return error.FileInaccessible; - - archive_entry.cursor = cursor; - } - - fn seekToEnd(context: *anyopaque) FileAccess.Error!void { - const archive_entry = archiveEntryCast(context); - - if (archive_entry.owner == null) return error.FileInaccessible; - - archive_entry.cursor = archive_entry.header.file_size; - } - - fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void { - const math = std.math; - const archive_entry = archiveEntryCast(context); - - if (archive_entry.owner == null) return error.FileInaccessible; - - if (offset < 0) { - archive_entry.cursor = math.max(0, - archive_entry.cursor - math.absCast(offset)); - } else { - archive_entry.cursor += @intCast(u64, offset); - } - } - }; - - if (archive.index_cache.lookup(path.path)) |index| { - archive.file_access.seek(index) catch return error.FileNotFound; - - entry.* = .{ - .owner = &archive.file_access, - .cursor = 0, - - .header = (oar.Entry.next(archive.file_access) catch return error.FileNotFound) orelse { - // Remove cannot fail if lookup succeeded. - std.debug.assert(archive.index_cache.remove(path.path) != null); - - return error.FileNotFound; - }, - }; - } else { - while (oar.Entry.next(archive.file_access) catch return error.FileNotFound) |entry_header| { - if (entry.header.path.equals(path.path)) - entry.* = .{ - .owner = &archive.file_access, - .cursor = 0, - .header = entry_header, - }; - } - - return error.FileNotFound; - } - - return FileAccess{ - .context = entry, - - .implementation = &.{ - .close = Implementation.close, - .queryCursor = Implementation.queryCursor, - .queryLength = Implementation.queryLength, - .read = Implementation.read, - .seek = Implementation.seek, - .seekToEnd = Implementation.seekToEnd, - .skip = Implementation.skip, - }, - }; - }; - - return error.OutOfFiles; - }, - - .native => |native| { - if (native.len == 0) return error.FileNotFound; - - var path_buffer = std.mem.zeroes([4096]u8); - const seperator_length = @boolToInt(native[native.len - 1] != seperator); - - if ((native.len + seperator_length + path.path.length) >= - path_buffer.len) return error.FileNotFound; - - 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], path.path.buffer[0 .. path.path.length]); - - ext.SDL_ClearError(); - - const FileAccess = ona.io.FileAccess; + const FileAccess = ona.io.FileAccess; + for (archive.entry_table) |*entry| if (entry.owner == null) { const Implementation = struct { - fn rwOpsCast(context: *anyopaque) *ext.SDL_RWops { - return @ptrCast(*ext.SDL_RWops, @alignCast( - @alignOf(ext.SDL_RWops), context)); + fn archiveEntryCast(context: *anyopaque) *Archive.Entry { + return @ptrCast(*Archive.Entry, @alignCast( + @alignOf(Archive.Entry), 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()); + archiveEntryCast(context).owner = null; } fn queryCursor(context: *anyopaque) FileAccess.Error!u64 { - ext.SDL_ClearError(); + const archive_entry = archiveEntryCast(context); - const sought = ext.SDL_RWtell(rwOpsCast(context)); + if (archive_entry.owner == null) return error.FileInaccessible; - if (sought < 0) return error.FileInaccessible; - - return @intCast(u64, sought); + return archive_entry.cursor; } fn queryLength(context: *anyopaque) FileAccess.Error!u64 { - ext.SDL_ClearError(); + const archive_entry = archiveEntryCast(context); - const sought = ext.SDL_RWsize(rwOpsCast(context)); + if (archive_entry.owner == null) return error.FileInaccessible; - if (sought < 0) return error.FileInaccessible; - - return @intCast(u64, sought); + return archive_entry.header.file_size; } fn read(context: *anyopaque, buffer: []u8) FileAccess.Error!usize { - ext.SDL_ClearError(); + const archive_entry = archiveEntryCast(context); - const buffer_read = ext.SDL_RWread(rwOpsCast( - context), buffer.ptr, @sizeOf(u8), buffer.len); - - if ((buffer_read == 0) and (ext.SDL_GetError() != null)) + const file_access = archive_entry.owner orelse return error.FileInaccessible; - return buffer_read; + if (archive_entry.cursor >= archive_entry.header.file_size) + return error.FileInaccessible; + + try file_access.seek(archive_entry.header.absolute_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 { - var to_seek = cursor; + const archive_entry = archiveEntryCast(context); - while (to_seek != 0) { - const math = std.math; - const sought = math.min(to_seek, math.maxInt(i64)); + if (archive_entry.owner == null) return error.FileInaccessible; - ext.SDL_ClearError(); - - if (ext.SDL_RWseek(rwOpsCast(context), @intCast(i64, sought), - ext.RW_SEEK_CUR) < 0) return error.FileInaccessible; - - to_seek -= sought; - } + archive_entry.cursor = cursor; } fn seekToEnd(context: *anyopaque) FileAccess.Error!void { - ext.SDL_ClearError(); + const archive_entry = archiveEntryCast(context); - if (ext.SDL_RWseek(rwOpsCast(context), 0, ext.RW_SEEK_END) < 0) - return error.FileInaccessible; + if (archive_entry.owner == null) return error.FileInaccessible; + + archive_entry.cursor = archive_entry.header.file_size; } fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void { - ext.SDL_ClearError(); + const math = std.math; + const archive_entry = archiveEntryCast(context); - if (ext.SDL_RWseek(rwOpsCast(context), offset, ext.RW_SEEK_SET) < 0) - return error.FileInaccessible; + if (archive_entry.owner == null) return error.FileInaccessible; + + if (offset < 0) { + archive_entry.cursor = math.max(0, + archive_entry.cursor - math.absCast(offset)); + } else { + archive_entry.cursor += @intCast(u64, offset); + } } }; + const Header = oar.Entry; + + if (archive.index_cache.lookup(path)) |index| { + archive.file_access.seek(index) catch return error.FileNotFound; + + entry.* = .{ + .owner = &archive.file_access, + .cursor = 0, + + .header = (Header.next(archive.file_access) catch + return error.FileNotFound) orelse { + + // Remove cannot fail if lookup succeeded. + std.debug.assert(archive.index_cache.remove(path) != null); + + return error.FileNotFound; + }, + }; + } else { + while (Header.next(archive.file_access) catch + return error.FileNotFound) |entry_header| { + + if (entry.header.path.equals(path)) entry.* = .{ + .owner = &archive.file_access, + .cursor = 0, + .header = entry_header, + }; + } + + return error.FileNotFound; + } + return FileAccess{ - .context = ext.SDL_RWFromFile(&path_buffer, switch (mode) { - .readonly => "rb", - .overwrite => "wb", - .append => "ab", - }) orelse return error.FileNotFound, + .context = entry, .implementation = &.{ .close = Implementation.close, @@ -584,69 +439,126 @@ pub const FileSystem = union(enum) { .skip = Implementation.skip, }, }; - }, - } - } - - pub const seperator = '/'; - }; - - /// - /// [PathError.TooLong] occurs when creating a path that is greater than the maximum size **in - /// bytes**. - /// - pub const PathError = error { - TooLong, - }; - - /// - /// 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 fn joinedPath(file_system: *FileSystem, sequences: []const []const u8) PathError!Path { - var path = Path{ - .file_system = file_system, - .path = oar.Path.empty, - }; - - if (sequences.len != 0) { - const last_sequence_index = sequences.len - 1; - - for (sequences) |sequence, index| if (sequence.len != 0) { - var components = ona.io.Spliterator(u8){ - .source = sequence, - .delimiter = "/", }; - while (components.next()) |component| if (component.len != 0) { - for (component) |byte| { - if (path.path.length == Path.max) return error.TooLong; + return error.OutOfFiles; + }, - path.path.buffer[path.path.length] = byte; - path.path.length += 1; + .native => |native| { + if (native.len == 0) return error.FileNotFound; + + var path_buffer = std.mem.zeroes([4096]u8); + const seperator_length = @boolToInt(native[native.len - 1] != oar.Path.seperator); + + if ((native.len + seperator_length + path.length) >= + path_buffer.len) return error.FileNotFound; + + std.mem.copy(u8, path_buffer[0 ..], native); + + if (seperator_length != 0) path_buffer[native.len] = oar.Path.seperator; + + std.mem.copy(u8, path_buffer[native.len .. path_buffer. + len], path.buffer[0 .. path.length]); + + const FileAccess = ona.io.FileAccess; + + const Implementation = struct { + fn rwOpsCast(context: *anyopaque) *ext.SDL_RWops { + return @ptrCast(*ext.SDL_RWops, @alignCast( + @alignOf(ext.SDL_RWops), context)); } - if (components.hasNext()) { - if (path.path.length == Path.max) return error.TooLong; + fn close(context: *anyopaque) void { + ext.SDL_ClearError(); - path.path.buffer[path.path.length] = '/'; - path.path.length += 1; + 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 = math.min(to_seek, math.maxInt(i64)); + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(rwOpsCast(context), @intCast(i64, sought), + ext.RW_SEEK_CUR) < 0) return error.FileInaccessible; + + to_seek -= 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; + } + + fn skip(context: *anyopaque, offset: i64) FileAccess.Error!void { + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(rwOpsCast(context), offset, ext.RW_SEEK_SET) < 0) + return error.FileInaccessible; } }; - if (index < last_sequence_index) { - if (path.path.length == Path.max) return error.TooLong; + ext.SDL_ClearError(); - path.path.buffer[path.path.length] = '/'; - path.path.length += 1; - } - }; + 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, + .skip = Implementation.skip, + }, + }; + }, } - - return path; } }; @@ -721,6 +633,11 @@ pub const Log = enum(u32) { } }; +/// +/// Path to a file on a [FileSystem]. +/// +pub const Path = oar.Path; + /// /// [RunError.InitFailure] occurs if a necessary resource fails to be acquired or allocated. /// @@ -774,7 +691,7 @@ pub fn runGraphics(comptime Error: anytype, defer ext.SDL_DestroyRenderer(renderer); var cwd_file_system = FileSystem{.native ="./"}; - var data_access = try (try cwd_file_system.joinedPath(&.{"./data.oar"})).open(.readonly); + var data_access = try cwd_file_system.open(try Path.joined(&.{"./data.oar"}), .readonly); defer data_access.close(); diff --git a/src/oar/main.zig b/src/oar/main.zig index 63ba2e0..e11d69e 100644 --- a/src/oar/main.zig +++ b/src/oar/main.zig @@ -55,6 +55,14 @@ pub const Path = extern struct { buffer: [255]u8, length: u8, + /// + /// [Error.TooLong] occurs when creating a path that is greater than the maximum path size **in + /// bytes**. + /// + pub const Error = error { + TooLong, + }; + /// /// An empty [Path] with a length of `0`. /// @@ -74,6 +82,63 @@ pub const Path = extern struct { pub fn hash(path: Path) usize { return ona.io.hashBytes(path.buffer[0 .. path.length]); } + + /// + /// Attempts to create a [Path] with the path components in `sequences` as a fully qualified + /// path from root. + /// + /// A [Path] value is returned containing the fully qualified path from the file-system root or + /// a [Error] if it could not be created. + /// + pub fn joined(sequences: []const []const u8) Error!Path { + var path = empty; + + if (sequences.len != 0) { + const last_sequence_index = sequences.len - 1; + + for (sequences) |sequence, index| if (sequence.len != 0) { + var components = ona.io.Spliterator(u8){ + .source = sequence, + .delimiter = "/", + }; + + while (components.next()) |component| if (component.len != 0) { + for (component) |byte| { + if (path.length == max) return error.TooLong; + + path.buffer[path.length] = byte; + path.length += 1; + } + + if (components.hasNext()) { + if (path.length == max) return error.TooLong; + + path.buffer[path.length] = '/'; + path.length += 1; + } + }; + + if (index < last_sequence_index) { + if (path.length == max) return error.TooLong; + + path.buffer[path.length] = '/'; + path.length += 1; + } + }; + } + + return path; + } + + /// + /// Maximum number of **bytes** in a [Path]. + /// + pub const max = 255; + + /// + /// Textual separator between components of a [Path]. + /// + pub const seperator = '/'; }; ///