511 lines
20 KiB
Zig
511 lines
20 KiB
Zig
// packerfs
|
|
//
|
|
// think of it as a single large file system archive
|
|
//
|
|
// that allows you to mount and unmount multiple packer files into memory.
|
|
// as well as discover files from a pak which aren't yet fully mounted, and
|
|
// keep track of them in a registry.
|
|
//
|
|
// hmm... how does one handle memory mapping.
|
|
//
|
|
// packerfs is the main way that we should be accessing files,
|
|
//
|
|
// when a file is discovered into the registry. we gain the option to load the file
|
|
// when we load the file it is mmapped into memory at an address and the pointer to bytes in the file is returned
|
|
//
|
|
// version 1:
|
|
// - discover files from paks
|
|
// - want a file, I ask for that file.
|
|
// - get back a mapping struct which contains a []const u8 bytes struct
|
|
// - this bytes struct tracks which archive it came from
|
|
// - when finished using the file, unmap the mapping struct,
|
|
// - if it was the last file to be unmapped,
|
|
// - then the file shapp be evicted from
|
|
|
|
const std = @import("std");
|
|
const p2 = @import("p2");
|
|
const Name = p2.Name;
|
|
const MakeName = p2.MakeName;
|
|
const PackedFileEntry = @import("PackedFileEntry.zig");
|
|
|
|
// this contains information about where the file came from.
|
|
const PakSourceRef = usize;
|
|
|
|
pub fn getDisplayNameForFileSource(source: PakSourceRef, packerfs: *const PackerFS) []const u8 {
|
|
return packerfs.pakMountings.items[source].filePath;
|
|
}
|
|
|
|
pub const PackerBytesMapping = struct {
|
|
bytes: []const u8,
|
|
mappingId: usize,
|
|
fileEntryId: usize,
|
|
inMemory: bool,
|
|
embedded: bool,
|
|
|
|
pub inline fn bytesNoEnd(self: @This()) []const u8 {
|
|
if (self.embedded) {
|
|
return self.bytes[0..self.bytes.len];
|
|
} else {
|
|
return self.bytes[0 .. self.bytes.len - 1];
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Settings = struct {
|
|
mountOnDemand: bool = true, // Disable this if we want to restrict file mounting to be manually mounted only.
|
|
// If set to false, PackerFS
|
|
allowContentFolderAccess: bool = true,
|
|
contentFolderExtraPaths: []const []const u8 = &[_][]const u8{}, // By default, this will mount the content/ folder on disk.
|
|
};
|
|
|
|
pub const AnyWatchCallback = struct {
|
|
func: *const fn ([]const u8, ?*anyopaque) void,
|
|
ctx: ?*anyopaque = null,
|
|
|
|
pub inline fn call(self: *@This(), path: []const u8) void {
|
|
self.func(path, self.ctx);
|
|
}
|
|
};
|
|
|
|
pub const WatchCallback = struct {
|
|
func: *const fn ([]const u8, ?*anyopaque) void,
|
|
ctx: ?*anyopaque = null,
|
|
path: []const u8,
|
|
|
|
pub inline fn call(self: *@This(), path: []const u8) void {
|
|
self.func(path, self.ctx);
|
|
}
|
|
};
|
|
|
|
pub const PackerFS = struct {
|
|
allocator: std.mem.Allocator,
|
|
fileHeaders: std.ArrayListUnmanaged(PackedFileEntry) = .{},
|
|
filePakSources: std.ArrayListUnmanaged(PakSourceRef) = .{},
|
|
fileHandlesByName: std.AutoHashMapUnmanaged(u32, usize) = .{},
|
|
|
|
pakMountings: std.ArrayListUnmanaged(PakMounting) = .{},
|
|
contentPaths: std.ArrayListUnmanaged([]u8) = .{},
|
|
|
|
stringArena: std.heap.ArenaAllocator,
|
|
settings: Settings = .{},
|
|
|
|
lock: std.Thread.Mutex = .{},
|
|
|
|
anyWatchCallbacks: std.ArrayListUnmanaged(AnyWatchCallback) = .{},
|
|
fileWatchCallbacks: std.ArrayListUnmanaged(WatchCallback) = .{},
|
|
|
|
pub const PakMounting = struct {
|
|
filePath: []const u8, // path to the file
|
|
bytes: []align(8) u8 = undefined,
|
|
mounted: bool = false,
|
|
mountRefs: std.ArrayListUnmanaged(usize) = .{},
|
|
contentOffset: usize = 0,
|
|
inMemory: bool = false,
|
|
embedded: bool = false,
|
|
|
|
pub fn isFileMounted(self: @This()) bool {
|
|
return self.mounted or self.embedded;
|
|
}
|
|
|
|
pub fn unmount(self: *@This(), allocator: std.mem.Allocator) void {
|
|
if (self.embedded) {
|
|
self.mountRefs.deinit(allocator);
|
|
self.mountRefs = .{};
|
|
return;
|
|
}
|
|
|
|
if (self.mounted) {
|
|
// std.debug.print("unmounting file:{s} bytes: 0x{x}\n", .{ self.filePath, @intFromPtr(self.bytes.ptr) });
|
|
allocator.free(self.bytes);
|
|
self.mountRefs.deinit(allocator);
|
|
self.mounted = false;
|
|
self.mountRefs = .{};
|
|
}
|
|
}
|
|
|
|
pub fn removeMapping(self: *@This(), allocator: std.mem.Allocator, fileRef: usize) void {
|
|
// should we go ahead with bookkeeping anyway?
|
|
if (self.embedded)
|
|
return;
|
|
|
|
for (0..self.mountRefs.items.len) |i| {
|
|
if (self.mountRefs.items[i] == fileRef) {
|
|
_ = self.mountRefs.swapRemove(i);
|
|
|
|
// TODO: move this into a sweep and clean operation
|
|
if (self.mountRefs.items.len == 0) {
|
|
self.unmount(allocator);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn addFileChangedCallback(
|
|
self: *@This(),
|
|
path: []const u8,
|
|
cb: *const fn ([]const u8, ?*anyopaque) void,
|
|
ctx: ?*anyopaque,
|
|
) !void {
|
|
const x = WatchCallback{
|
|
.func = cb,
|
|
.ctx = ctx,
|
|
.path = path,
|
|
};
|
|
|
|
try self.fileWatchCallbacks.append(self.allocator, x);
|
|
}
|
|
|
|
// sets up a callback for if ANY file changes
|
|
pub fn addAnyWatchCallback(self: *@This(), callback: *const fn (path: []const u8, ctx: ?*anyopaque) void, ctx: ?*anyopaque) !void {
|
|
try self.anyWatchCallbacks.append(self.allocator, .{ .func = callback, .ctx = ctx });
|
|
}
|
|
|
|
pub fn watchCallback(path: [*c]const u8, ctx: ?*anyopaque) callconv(.c) void {
|
|
const self: *@This() = @ptrCast(@alignCast(ctx));
|
|
|
|
for (self.fileWatchCallbacks.items) |*watch| {
|
|
std.debug.print("checking {s} {s}\n", .{ watch.path, path });
|
|
if (std.mem.eql(u8, watch.path, std.mem.span(path))) {
|
|
watch.call(std.mem.span(path));
|
|
}
|
|
std.debug.print("registered callback: {s}\n", .{watch.path});
|
|
}
|
|
|
|
for (self.anyWatchCallbacks.items) |*watch| {
|
|
// std.debug.print("anywatch callback: {s}\n", .{watch.path});
|
|
watch.call(std.mem.span(path));
|
|
}
|
|
}
|
|
|
|
// todo: implement load memory to file then-map setup.
|
|
// for systems which do not support mmap
|
|
|
|
pub fn init(allocator: std.mem.Allocator, settings: Settings) !*@This() {
|
|
const self = try allocator.create(@This());
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.settings = settings,
|
|
.stringArena = std.heap.ArenaAllocator.init(allocator),
|
|
};
|
|
|
|
try self.addContentPath("content");
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn discoverFromFile(self: *@This(), filePath: []const u8) !void {
|
|
self.lock.lock();
|
|
defer self.lock.unlock();
|
|
|
|
// std.debug.print("discovered from file: {s}\n", .{filePath});
|
|
|
|
// load the file and read out all headers from the file at the path
|
|
const file = try std.fs.cwd().openFile(filePath, .{});
|
|
defer file.close();
|
|
|
|
const buf = try self.allocator.alloc(u8, 8192);
|
|
defer self.allocator.free(buf);
|
|
|
|
var reader = file.reader(buf);
|
|
var iterator = try PackedFileEntry.ReaderIterator.init(&reader.interface);
|
|
|
|
const pakMountingIndex = self.pakMountings.items.len;
|
|
|
|
try self.pakMountings.append(self.allocator, .{ .filePath = try self.stringAlloc().dupe(u8, filePath) });
|
|
|
|
while (try iterator.next(&reader.interface)) |headerEntry| {
|
|
var HeaderName = Name.Make(headerEntry.getFileName());
|
|
|
|
if (self.fileHandlesByName.get(HeaderName.handle())) |oldFileHeaderIndex| {
|
|
self.fileHeaders.items[oldFileHeaderIndex] = headerEntry;
|
|
self.filePakSources.items[oldFileHeaderIndex] = pakMountingIndex;
|
|
} else {
|
|
_ = try self.addFileEntry(headerEntry, pakMountingIndex);
|
|
}
|
|
}
|
|
self.pakMountings.items[pakMountingIndex].contentOffset = iterator.bytesRead;
|
|
}
|
|
|
|
pub fn countFilesDiscovered(self: @This()) u64 {
|
|
return @intCast(self.fileHeaders.items.len);
|
|
}
|
|
|
|
pub fn resolveContentFile(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !?[]u8 {
|
|
for (self.contentPaths.items) |contentPath| {
|
|
// std.debug.print("{s}\n", .{contentPath});
|
|
const fullPath = try std.fmt.allocPrint(self.stringArena.allocator(), "{s}/{s}", .{ contentPath, path });
|
|
defer self.stringArena.allocator().free(fullPath);
|
|
|
|
var exists: bool = true;
|
|
|
|
std.fs.cwd().access(fullPath, .{}) catch |err| {
|
|
exists = if (err == error.FileNotFound) false else true;
|
|
};
|
|
|
|
if (exists) {
|
|
return try allocator.dupe(u8, fullPath);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn fileExists(self: *@This(), path: []const u8) bool {
|
|
self.lock.lock();
|
|
defer self.lock.unlock();
|
|
var pathName = MakeName(path);
|
|
if (self.fileHandlesByName.get(pathName.handle())) |handle| {
|
|
_ = handle;
|
|
return true;
|
|
}
|
|
|
|
if (!self.settings.allowContentFolderAccess) {
|
|
return false;
|
|
}
|
|
|
|
for (self.contentPaths.items) |contentPath| {
|
|
if (self.loadFileDirect(contentPath, path) catch return false) |mapping| {
|
|
self.pakMountings.items[mapping.mappingId].removeMapping(self.allocator, mapping.fileEntryId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn loadFile(self: *@This(), path: []const u8) !PackerBytesMapping {
|
|
self.lock.lock();
|
|
defer self.lock.unlock();
|
|
|
|
// std.debug.print("loading file: {s}", .{path});
|
|
var pathName = Name.Make(path);
|
|
if (self.fileHandlesByName.get(pathName.handle())) |fileNameHandle| {
|
|
// std.debug.print("{s} maps to index {d}\n", .{ path, fileNameHandle });
|
|
if (try self.loadFileByIndexFromPak(fileNameHandle)) |mapping| {
|
|
return mapping;
|
|
}
|
|
}
|
|
|
|
if (!self.settings.allowContentFolderAccess) {
|
|
return error.FileNotPackaged;
|
|
}
|
|
|
|
for (self.contentPaths.items) |contentPath| {
|
|
if (try self.loadFileDirect(contentPath, path)) |mapping| {
|
|
// std.debug.print("loading directly: {s}, {s}", .{ contentPath, path });
|
|
return mapping;
|
|
}
|
|
}
|
|
|
|
// std.debug.print("PackerFS - Error: FileNotFound {s}", .{path});
|
|
return error.FileNotFound;
|
|
}
|
|
|
|
fn addFileEntry(self: *@This(), headerEntry: PackedFileEntry, pakMountingIndex: usize) !usize {
|
|
var headerName = Name.Make(headerEntry.getFileName());
|
|
const headerIndex = self.fileHeaders.items.len;
|
|
try self.fileHeaders.append(self.allocator, headerEntry);
|
|
try self.filePakSources.append(self.allocator, pakMountingIndex);
|
|
|
|
// std.debug.print("name {s} nameIndex {d} headerIndex {d}", .{ headerName.utf8(), headerName.handle(), headerIndex });
|
|
try self.fileHandlesByName.put(self.allocator, headerName.handle(), headerIndex);
|
|
|
|
try p2.assert(self.filePakSources.items.len == self.fileHeaders.items.len);
|
|
|
|
return headerIndex;
|
|
}
|
|
|
|
fn stringAlloc(self: *@This()) std.mem.Allocator {
|
|
return self.stringArena.allocator();
|
|
}
|
|
|
|
// if skipLoading is set bytes will contain an empty path
|
|
fn loadFileDirect(self: *@This(), basePath: []const u8, path: []const u8) !?PackerBytesMapping {
|
|
// if we made it to this function we can assume that the file does not exist in existing pak mounting references.
|
|
// nor does it exist in self.fileHandlesByName
|
|
// we can add a new file reference, AND a new pakMounting entry here.
|
|
const fullPath = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ basePath, path });
|
|
defer self.allocator.free(fullPath);
|
|
|
|
const fileBytes: []align(8) u8 = @alignCast(p2.loadFileAlloc(fullPath, .@"8", self.allocator) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
return null;
|
|
},
|
|
else => |narrow| {
|
|
return narrow;
|
|
},
|
|
});
|
|
|
|
return try self.installFileBytesMount(fullPath, fileBytes, false);
|
|
}
|
|
|
|
pub fn installFileBytesMount(self: *@This(), path: []const u8, fileBytes: []align(8) u8, embedded: bool) !?PackerBytesMapping {
|
|
const pakMountIndex = self.pakMountings.items.len;
|
|
try self.pakMountings.append(self.allocator, .{
|
|
.filePath = try self.stringAlloc().dupe(u8, path),
|
|
.inMemory = true,
|
|
});
|
|
|
|
try p2.assert(self.filePakSources.items.len == self.fileHeaders.items.len);
|
|
|
|
// generate a header for the thing
|
|
const headerEntry = try PackedFileEntry.init("unknown", path, 0, fileBytes.len);
|
|
const headerIndex = try self.addFileEntry(headerEntry, pakMountIndex);
|
|
|
|
self.pakMountings.items[pakMountIndex].bytes = fileBytes;
|
|
try self.pakMountings.items[pakMountIndex].mountRefs.append(self.allocator, headerIndex);
|
|
self.pakMountings.items[pakMountIndex].mounted = true;
|
|
self.pakMountings.items[pakMountIndex].embedded = embedded;
|
|
|
|
// std.debug.print("loadFileDirect {s} {d} 0x{x}\n", .{ path, headerIndex, @intFromPtr(fileBytes.ptr) });
|
|
return .{
|
|
.bytes = fileBytes,
|
|
.mappingId = pakMountIndex,
|
|
.fileEntryId = headerIndex,
|
|
.embedded = embedded,
|
|
.inMemory = true,
|
|
};
|
|
}
|
|
|
|
// if the file is not loaded, then mount the entire package then load out the bytes
|
|
// TODO: implement file memory mapping on windows, or cook up a really good packaging setup.
|
|
fn loadFileByIndexFromPak(self: *@This(), index: usize) !?PackerBytesMapping {
|
|
|
|
// grab the file header,
|
|
const source = self.filePakSources.items[index];
|
|
const header = self.fileHeaders.items[index];
|
|
|
|
// grab the file source,
|
|
const pakMountingRef = &self.pakMountings.items[source];
|
|
|
|
if (!pakMountingRef.isFileMounted()) {
|
|
// std.debug.print("mounting file: {s}\n", .{pakMountingRef.filePath});
|
|
// std.debug.print("loading mount ref {s}\n", .{pakMountingRef.filePath});
|
|
const fileBytes: []align(8) u8 = @alignCast(p2.loadFileAlloc(pakMountingRef.filePath, .@"8", self.allocator) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
return null;
|
|
},
|
|
error.InvalidWtf8 => {
|
|
// std.debug.print("failed to load path INVALIDWTF8 {s} {d}\n", .{ pakMountingRef.filePath, index });
|
|
return null;
|
|
},
|
|
else => |narrow| {
|
|
return narrow;
|
|
},
|
|
});
|
|
|
|
//pakMountingRef.*.bytes = @alignCast(p2.loadFileAlloc(pakMountingRef.filePath, 8, self.allocator) catch return null);
|
|
pakMountingRef.*.bytes = fileBytes;
|
|
}
|
|
|
|
const offset = header.fileOffset + pakMountingRef.contentOffset;
|
|
try self.pakMountings.items[source].mountRefs.append(self.allocator, index);
|
|
self.pakMountings.items[source].mounted = true;
|
|
|
|
return PackerBytesMapping{
|
|
.bytes = pakMountingRef.bytes[offset .. offset + header.fileLen],
|
|
.mappingId = source,
|
|
.fileEntryId = index,
|
|
.embedded = self.pakMountings.items[source].embedded,
|
|
.inMemory = pakMountingRef.inMemory,
|
|
};
|
|
}
|
|
|
|
pub fn unmap(self: *@This(), mapping: PackerBytesMapping) void {
|
|
self.lock.lock();
|
|
defer self.lock.unlock();
|
|
|
|
// std.debug.print("unmapping file: {d},{d} 0x{x}\n", .{ mapping.mappingId, mapping.fileEntryId, @intFromPtr(mapping.bytes.ptr) });
|
|
self.pakMountings.items[mapping.mappingId].removeMapping(self.allocator, mapping.fileEntryId);
|
|
return;
|
|
}
|
|
|
|
pub fn destroy(self: *@This()) void {
|
|
self.lock.lock();
|
|
|
|
for (self.pakMountings.items) |*mounting| {
|
|
mounting.unmount(self.allocator);
|
|
}
|
|
|
|
self.fileHeaders.deinit(self.allocator);
|
|
self.fileHandlesByName.deinit(self.allocator);
|
|
self.filePakSources.deinit(self.allocator);
|
|
self.pakMountings.deinit(self.allocator);
|
|
|
|
for (self.contentPaths.items) |path| {
|
|
self.allocator.free(path);
|
|
}
|
|
self.contentPaths.deinit(self.allocator);
|
|
self.stringArena.deinit();
|
|
self.lock.unlock();
|
|
self.fileWatchCallbacks.deinit(self.allocator);
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
pub fn addContentPath(self: *@This(), path: []const u8) !void {
|
|
try self.contentPaths.append(self.allocator, try p2.dupeString(self.allocator, path));
|
|
}
|
|
|
|
pub const PathListResults = struct {
|
|
allocator: std.mem.Allocator,
|
|
data: std.ArrayListUnmanaged([]const u8) = .{},
|
|
sources: std.ArrayListUnmanaged([]const u8) = .{},
|
|
|
|
pub fn deinit(self: *@This()) void {
|
|
for (self.data.items, 0..) |item, i| {
|
|
self.allocator.free(item);
|
|
self.allocator.free(self.sources.items[i]);
|
|
}
|
|
self.data.deinit(self.allocator);
|
|
self.sources.deinit(self.allocator);
|
|
}
|
|
};
|
|
|
|
pub fn listAllSubpaths(self: @This(), allocator: std.mem.Allocator, subpath: []const u8) !PathListResults {
|
|
var dupeMap: std.StringHashMapUnmanaged(bool) = .{};
|
|
defer dupeMap.deinit(allocator);
|
|
|
|
var rv = std.ArrayListUnmanaged([]const u8){};
|
|
var sourcePath = std.ArrayListUnmanaged([]const u8){};
|
|
// std.debug.print("listing all subpaths under {s}:\n", .{subpath});
|
|
|
|
// list all files discovered from pak files that match the subpath
|
|
for (self.fileHeaders.items) |f| {
|
|
if (std.mem.startsWith(u8, f.getFileName(), subpath)) {
|
|
// std.debug.print(" [packed] p = {s}\n", .{f.getFileName()});
|
|
|
|
// Is this my excuse now to implement a string bump allocator?
|
|
try rv.append(allocator, try p2.dupeString(allocator, f.getFileName()));
|
|
try sourcePath.append(allocator, try p2.dupeString(allocator, "PACKED_ARCHIVE"));
|
|
try dupeMap.put(allocator, try p2.dupeString(allocator, f.getFileName()), true);
|
|
}
|
|
}
|
|
|
|
// std.debug.print("content paths {s}: \n", .{subpath});
|
|
for (self.contentPaths.items) |contentPath| {
|
|
const apath = try std.fs.cwd().openDir(contentPath, .{ .iterate = true });
|
|
var walker = try apath.walk(allocator);
|
|
defer walker.deinit();
|
|
while (try walker.next()) |p| {
|
|
if (p.kind == .file and !dupeMap.contains(p.path) and std.mem.startsWith(u8, p.path, subpath)) {
|
|
// std.debug.print(" p = {s}\n", .{p.path});
|
|
try rv.append(allocator, try p2.dupeString(allocator, p.path));
|
|
try sourcePath.append(allocator, try p2.dupeString(allocator, contentPath));
|
|
try dupeMap.put(allocator, try p2.dupeString(allocator, p.path), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
var i = dupeMap.iterator();
|
|
while (i.next()) |n| {
|
|
allocator.free(n.key_ptr.*);
|
|
}
|
|
}
|
|
|
|
return .{ .allocator = allocator, .data = rv, .sources = sourcePath };
|
|
}
|
|
};
|