Support direct browsing of a binary export

Code is more hacky than I prefer, but this approach does work and isn't
even as involved as I had anticipated.

Still a few known bugs and limitations left to resolve.
This commit is contained in:
Yorhel 2024-08-06 09:46:17 +02:00
parent 8fb2290d5e
commit 30d6ddf149
7 changed files with 235 additions and 85 deletions

View file

@ -188,6 +188,10 @@ const CborVal = struct {
}
}
fn isTrue(v: *const CborVal) bool {
return v.major == .simple and v.arg == 21;
}
// Read either a byte or text string.
// Doesn't validate UTF-8 strings, doesn't support indefinite-length strings.
fn bytes(v: *const CborVal) []const u8 {
@ -379,7 +383,7 @@ const Import = struct {
.asize => ctx.stat.size = kv.val.int(u64),
.dsize => ctx.stat.blocks = @intCast(kv.val.int(u64)/512),
.dev => ctx.stat.dev = kv.val.int(u64),
.rderr => ctx.fields.rderr = kv.val.major == .simple and kv.val.arg == 21,
.rderr => ctx.fields.rderr = kv.val.isTrue(),
.sub => ctx.fields.sub = kv.val.itemref(ref),
.ino => ctx.stat.ino = kv.val.int(u64),
.nlink => ctx.stat.nlink = kv.val.int(u31),
@ -427,6 +431,61 @@ const Import = struct {
}
};
// Resolve an itemref and return a newly allocated entry.
// Dir.parent and Link.next/prev are left uninitialized.
pub fn get(ref: u64, alloc: std.mem.Allocator) *model.Entry {
const parser = readItem(ref);
var etype: ?model.EType = null;
var name: []const u8 = "";
var p = parser;
while (p.next()) |kv| {
switch (kv.key) {
.type => etype = kv.val.etype(),
.name => name = kv.val.bytes(),
else => kv.val.skip(),
}
if (etype != null and name.len != 0) break;
}
if (etype == null or name.len == 0) die();
// XXX: 'extended' should really depend on whether the info is in the file.
var entry = model.Entry.create(alloc, etype.?, main.config.extended, name);
entry.next = .{ .ref = std.math.maxInt(u64) };
if (entry.dir()) |d| d.sub = .{ .ref = std.math.maxInt(u64) };
while (p.next()) |kv| switch (kv.key) {
.prev => entry.next = .{ .ref = kv.val.itemref(ref) },
.asize => { if (entry.pack.etype != .dir) entry.size = kv.val.int(u64); },
.dsize => { if (entry.pack.etype != .dir) entry.pack.blocks = @intCast(kv.val.int(u64)/512); },
.rderr => { if (entry.dir()) |d| {
if (kv.val.isTrue()) d.pack.err = true
else d.pack.suberr = true;
} },
.dev => { if (entry.dir()) |d| d.pack.dev = model.devices.getId(kv.val.int(u64)); },
.cumasize => entry.size = kv.val.int(u64),
.cumdsize => entry.pack.blocks = @intCast(kv.val.int(u64)/512),
.shrasize => { if (entry.dir()) |d| d.shared_size = kv.val.int(u64); },
.shrdsize => { if (entry.dir()) |d| d.shared_blocks = kv.val.int(u64)/512; },
.items => { if (entry.dir()) |d| d.items = kv.val.int(u32); },
.sub => { if (entry.dir()) |d| d.sub = .{ .ref = kv.val.itemref(ref) }; },
.ino => { if (entry.link()) |l| l.ino = kv.val.int(u64); },
.nlink => { if (entry.link()) |l| l.pack.nlink = kv.val.int(u31); },
.uid => { if (entry.ext()) |e| e.uid = kv.val.int(u32); },
.gid => { if (entry.ext()) |e| e.gid = kv.val.int(u32); },
.mode => { if (entry.ext()) |e| e.mode = kv.val.int(u16); },
.mtime => { if (entry.ext()) |e| e.mtime = kv.val.int(u64); },
else => kv.val.skip(),
};
return entry;
}
pub fn getRoot() u64 {
return bigu64(global.index[global.index.len-8..][0..8].*);
}
// Walk through the directory tree in depth-first order and pass results to sink.zig.
// Depth-first is required for JSON export, but more efficient strategies are
// possible for other sinks. Parallel import is also an option, but that's more
@ -434,7 +493,7 @@ const Import = struct {
pub fn import() void {
const sink_threads = sink.createThreads(1);
var ctx = Import{.sink = &sink_threads[0]};
ctx.import(bigu64(global.index[global.index.len-8..][0..8].*), null, 0);
ctx.import(getRoot(), null, 0);
sink.done();
}

View file

@ -6,6 +6,7 @@ const main = @import("main.zig");
const model = @import("model.zig");
const sink = @import("sink.zig");
const mem_sink = @import("mem_sink.zig");
const bin_reader = @import("bin_reader.zig");
const delete = @import("delete.zig");
const ui = @import("ui.zig");
const c = @cImport(@cInclude("time.h"));
@ -13,6 +14,13 @@ const util = @import("util.zig");
// Currently opened directory.
pub var dir_parent: *model.Dir = undefined;
pub var dir_path: [:0]u8 = undefined;
var dir_parents = std.ArrayList(model.Ref).init(main.allocator);
var dir_alloc = std.heap.ArenaAllocator.init(main.allocator);
// Used to keep track of which dir is which ref, so we can enter it.
// Only used for binreader browsing.
var dir_refs = std.ArrayList(struct { ptr: *model.Dir, ref: u64 }).init(main.allocator);
// Sorted list of all items in the currently opened directory.
// (first item may be null to indicate the "parent directory" item)
@ -33,28 +41,28 @@ const View = struct {
// The hash(name) of the selected entry (cursor), this is used to derive
// cursor_idx after sorting or changing directory.
// (collisions may cause the wrong entry to be selected, but dealing with
// string allocations sucks and I expect collisions to be rare enough)
cursor_hash: u64 = 0,
fn hashEntry(entry: ?*model.Entry) u64 {
return if (entry) |e| std.hash.Wyhash.hash(0, e.name()) else 0;
fn dirHash() u64 {
return std.hash.Wyhash.hash(0, dir_path);
}
// Update cursor_hash and save the current view to the hash table.
fn save(self: *@This()) void {
self.cursor_hash = if (dir_items.items.len == 0) 0
else hashEntry(dir_items.items[cursor_idx]);
opened_dir_views.put(@intFromPtr(dir_parent), self.*) catch {};
else if (dir_items.items[cursor_idx]) |e| e.nameHash()
else 0;
opened_dir_views.put(dirHash(), self.*) catch {};
}
// Should be called after dir_parent or dir_items has changed, will load the last saved view and find the proper cursor_idx.
fn load(self: *@This(), sel: ?*const model.Entry) void {
if (opened_dir_views.get(@intFromPtr(dir_parent))) |v| self.* = v
fn load(self: *@This(), sel: u64) void {
if (opened_dir_views.get(dirHash())) |v| self.* = v
else self.* = @This(){};
cursor_idx = 0;
for (dir_items.items, 0..) |e, i| {
if (if (sel != null) e == sel else self.cursor_hash == hashEntry(e)) {
const h = if (e) |x| x.nameHash() else 0;
if (if (sel != 0) h == sel else self.cursor_hash == h) {
cursor_idx = i;
break;
}
@ -65,10 +73,8 @@ const View = struct {
var current_view = View{};
// Directories the user has browsed to before, and which item was last selected.
// The key is the @intFromPtr() of the opened *Dir; An int because the pointer
// itself may have gone stale after deletion or refreshing. They're only for
// lookups, not dereferencing.
var opened_dir_views = std.AutoHashMap(usize, View).init(main.allocator);
// The key is the hash of dir_path;
var opened_dir_views = std.AutoHashMap(u64, View).init(main.allocator);
fn sortIntLt(a: anytype, b: @TypeOf(a)) ?bool {
return if (a == b) null else if (main.config.sort_order == .asc) a < b else a > b;
@ -114,7 +120,7 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
// - config.sort_* changes
// - dir_items changes (i.e. from loadDir())
// - files in this dir have changed in a way that affects their ordering
fn sortDir(next_sel: ?*const model.Entry) void {
fn sortDir(next_sel: u64) void {
// No need to sort the first item if that's the parent dir reference,
// excluding that allows sortLt() to ignore null values.
const lst = dir_items.items[(if (dir_items.items.len > 0 and dir_items.items[0] == null) @as(usize, 1) else 0)..];
@ -126,16 +132,22 @@ fn sortDir(next_sel: ?*const model.Entry) void {
// - dir_parent changes (i.e. we change directory)
// - config.show_hidden changes
// - files in this dir have been added or removed
pub fn loadDir(next_sel: ?*const model.Entry) void {
pub fn loadDir(next_sel: u64) void {
_ = dir_alloc.reset(.free_all);
dir_items.shrinkRetainingCapacity(0);
dir_refs.shrinkRetainingCapacity(0);
dir_max_size = 1;
dir_max_blocks = 1;
dir_has_shared = false;
if (dir_parent != model.root)
if (dir_parents.items.len > 1)
dir_items.append(null) catch unreachable;
var it = dir_parent.sub;
while (it) |e| : (it = e.next) {
var ref = dir_parent.sub;
while (!ref.isNull()) {
const e =
if (main.config.binreader) bin_reader.get(ref.ref, dir_alloc.allocator())
else ref.ptr.?;
if (e.pack.blocks > dir_max_blocks) dir_max_blocks = e.pack.blocks;
if (e.size > dir_max_size) dir_max_size = e.size;
const shown = main.config.show_hidden or blk: {
@ -148,12 +160,67 @@ pub fn loadDir(next_sel: ?*const model.Entry) void {
};
if (shown) {
dir_items.append(e) catch unreachable;
if (e.dir()) |d| if (d.shared_blocks > 0 or d.shared_size > 0) { dir_has_shared = true; };
if (e.dir()) |d| {
if (d.shared_blocks > 0 or d.shared_size > 0) dir_has_shared = true;
if (main.config.binreader) dir_refs.append(.{ .ptr = d, .ref = ref.ref }) catch unreachable;
}
}
ref = e.next;
}
sortDir(next_sel);
}
pub fn initRoot() void {
if (main.config.binreader) {
const ref = bin_reader.getRoot();
dir_parent = bin_reader.get(ref, main.allocator).dir() orelse ui.die("Invalid import\n", .{});
dir_parents.append(.{ .ref = ref }) catch unreachable;
} else {
dir_parent = model.root;
dir_parents.append(.{ .ptr = &dir_parent.entry }) catch unreachable;
}
dir_path = main.allocator.dupeZ(u8, dir_parent.entry.name()) catch unreachable;
loadDir(0);
}
fn enterSub(e: *model.Dir) void {
if (main.config.binreader) {
const ref = blk: {
for (dir_refs.items) |r| if (r.ptr == e) break :blk r.ref;
return;
};
dir_parent.entry.destroy(main.allocator);
dir_parent = bin_reader.get(ref, main.allocator).dir() orelse unreachable;
dir_parents.append(.{ .ref = ref }) catch unreachable;
} else {
dir_parent = e;
dir_parents.append(.{ .ptr = &e.entry }) catch unreachable;
}
const newpath = std.fs.path.joinZ(main.allocator, &[_][]const u8{ dir_path, e.entry.name() }) catch unreachable;
main.allocator.free(dir_path);
dir_path = newpath;
}
fn enterParent() void {
std.debug.assert(dir_parents.items.len > 1);
_ = dir_parents.pop();
const p = dir_parents.items[dir_parents.items.len-1];
if (main.config.binreader) {
dir_parent.entry.destroy(main.allocator);
dir_parent = bin_reader.get(p.ref, main.allocator).dir() orelse unreachable;
} else
dir_parent = p.ptr.?.dir() orelse unreachable;
const newpath = main.allocator.dupeZ(u8, std.fs.path.dirname(dir_path) orelse unreachable) catch unreachable;
main.allocator.free(dir_path);
dir_path = newpath;
}
const Row = struct {
row: u32,
col: u32 = 0,
@ -168,7 +235,7 @@ const Row = struct {
const ch: u7 = switch (item.pack.etype) {
.dir => if (item.dir().?.pack.err) '!'
else if (item.dir().?.pack.suberr) '.'
else if (item.dir().?.sub == null) 'e'
else if (item.dir().?.sub.isNull()) 'e'
else return,
.link => 'H',
.pattern => '<',
@ -561,7 +628,7 @@ const info = struct {
if (ch == 10) { // Enter - go to selected entry
const l = links.?.items[links_idx];
dir_parent = l.parent;
loadDir(&l.entry);
loadDir(l.entry.nameHash());
set(null, .info);
}
}
@ -748,12 +815,7 @@ pub fn draw() void {
ui.move(1,3);
ui.addch(' ');
ui.style(.dir);
var pathbuf = std.ArrayList(u8).init(main.allocator);
dir_parent.fmtPath(true, &pathbuf);
ui.addstr(ui.shorten(ui.toUtf8(util.arrayListBufZ(&pathbuf)), ui.cols -| 5));
pathbuf.deinit();
ui.addstr(ui.shorten(ui.toUtf8(dir_path), ui.cols -| 5));
ui.style(.default);
ui.addch(' ');
@ -811,7 +873,7 @@ fn sortToggle(col: main.config.SortCol, default_order: main.config.SortOrder) vo
else if (main.config.sort_order == .asc) main.config.sort_order = .desc
else main.config.sort_order = .asc;
main.config.sort_col = col;
sortDir(null);
sortDir(0);
}
fn keyInputSelection(ch: i32, idx: *usize, len: usize, page: u32) bool {
@ -886,22 +948,22 @@ pub fn keyInput(ch: i32) void {
'M' => if (main.config.extended) sortToggle(.mtime, .desc),
'e' => {
main.config.show_hidden = !main.config.show_hidden;
loadDir(null);
loadDir(0);
state = .main;
},
't' => {
main.config.sort_dirsfirst = !main.config.sort_dirsfirst;
sortDir(null);
sortDir(0);
},
'a' => {
main.config.show_blocks = !main.config.show_blocks;
if (main.config.show_blocks and main.config.sort_col == .size) {
main.config.sort_col = .blocks;
sortDir(null);
sortDir(0);
}
if (!main.config.show_blocks and main.config.sort_col == .blocks) {
main.config.sort_col = .size;
sortDir(null);
sortDir(0);
}
},
@ -910,21 +972,22 @@ pub fn keyInput(ch: i32) void {
if (dir_items.items.len == 0) {
} else if (dir_items.items[cursor_idx]) |e| {
if (e.dir()) |d| {
dir_parent = d;
loadDir(null);
enterSub(d);
//dir_parent = d;
loadDir(0);
state = .main;
}
} else if (dir_parent.parent) |p| {
dir_parent = p;
loadDir(null);
} else if (dir_parents.items.len > 1) {
enterParent();
loadDir(0);
state = .main;
}
},
'h', '<', ui.c.KEY_BACKSPACE, ui.c.KEY_LEFT => {
if (dir_parent.parent) |p| {
const e = dir_parent;
dir_parent = p;
loadDir(&e.entry);
if (dir_parents.items.len > 1) {
//const h = dir_parent.entry.nameHash();
enterParent();
loadDir(0);
state = .main;
}
},

View file

@ -46,7 +46,7 @@ fn deleteItem(dir: std.fs.Dir, path: [:0]const u8, ptr: *align(1) ?*model.Entry)
if (entry.dir()) |d| {
var fd = dir.openDirZ(path, .{ .no_follow = true, .iterate = false }) catch |e| return err(e);
var it = &d.sub;
var it = &d.sub.ptr;
parent = d;
defer parent = parent.parent.?;
while (it.*) |n| {
@ -55,15 +55,15 @@ fn deleteItem(dir: std.fs.Dir, path: [:0]const u8, ptr: *align(1) ?*model.Entry)
return true;
}
if (it.* == n) // item deletion failed, make sure to still advance to next
it = &n.next;
it = &n.next.ptr;
}
fd.close();
dir.deleteDirZ(path) catch |e|
return if (e != error.DirNotEmpty or d.sub == null) err(e) else false;
return if (e != error.DirNotEmpty or d.sub.ptr == null) err(e) else false;
} else
dir.deleteFileZ(path) catch |e| return err(e);
ptr.*.?.zeroStats(parent);
ptr.* = ptr.*.?.next;
ptr.* = ptr.*.?.next.ptr;
return false;
}
@ -76,8 +76,8 @@ pub fn delete() ?*model.Entry {
// Find the pointer to this entry
const e = entry;
var it = &parent.sub;
while (it.*) |n| : (it = &n.next)
var it = &parent.sub.ptr;
while (it.*) |n| : (it = &n.next.ptr)
if (it.* == entry)
break;

View file

@ -95,6 +95,7 @@ pub const config = struct {
pub var sort_natural: bool = true;
pub var imported: bool = false;
pub var binreader: bool = false;
pub var can_delete: ?bool = null;
pub var can_shell: ?bool = null;
pub var can_refresh: ?bool = null;
@ -373,10 +374,6 @@ fn spawnShell() void {
ui.deinit();
defer ui.init();
var path = std.ArrayList(u8).init(allocator);
defer path.deinit();
browser.dir_parent.fmtPath(true, &path);
var env = std.process.getEnvMap(allocator) catch unreachable;
defer env.deinit();
// NCDU_LEVEL can only count to 9, keeps the implementation simple.
@ -391,7 +388,7 @@ fn spawnShell() void {
const shell = std.posix.getenvZ("NCDU_SHELL") orelse std.posix.getenvZ("SHELL") orelse "/bin/sh";
var child = std.process.Child.init(&.{shell}, allocator);
child.cwd = path.items;
child.cwd = browser.dir_path;
child.env_map = &env;
const stdin = std.io.getStdIn();
@ -451,16 +448,18 @@ fn readImport(path: [:0]const u8) !void {
const fd =
if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
else try std.fs.cwd().openFileZ(path, .{});
defer fd.close();
errdefer fd.close();
// TODO: While we're at it, recognize and handle compressed JSON
var buf: [8]u8 = undefined;
try fd.reader().readNoEof(&buf);
if (std.mem.eql(u8, &buf, bin_export.SIGNATURE)) {
try bin_reader.open(fd);
bin_reader.import();
} else
config.binreader = true;
} else {
json_import.import(fd, &buf);
fd.close();
}
}
pub fn main() void {
@ -571,6 +570,8 @@ pub fn main() void {
if (import_file) |f| {
readImport(f) catch |e| ui.die("Error reading file '{s}': {s}.\n", .{f, ui.errorString(e)});
config.imported = true;
if (config.binreader and export_json != null or export_bin != null)
bin_reader.import();
} else {
var buf = [_]u8{0} ** (std.fs.MAX_PATH_BYTES+1);
const path =
@ -587,8 +588,7 @@ pub fn main() void {
config.scan_ui = .full; // in case we're refreshing from the UI, always in full mode.
ui.init();
state = .browse;
browser.dir_parent = model.root;
browser.loadDir(null);
browser.initRoot();
while (true) {
switch (state) {
@ -602,7 +602,7 @@ pub fn main() void {
while (state == .refresh) handleEvent(true, true);
};
state = .browse;
browser.loadDir(null);
browser.loadDir(0);
},
.shell => {
spawnShell();
@ -611,7 +611,7 @@ pub fn main() void {
.delete => {
const next = delete.delete();
state = .browse;
browser.loadDir(next);
browser.loadDir(if (next) |n| n.nameHash() else 0);
},
else => handleEvent(true, false)
}

View file

@ -62,12 +62,12 @@ pub const Dir = struct {
};
var count: Map.Size = 0;
var it = dir.sub;
while (it) |e| : (it = e.next) count += 1;
var it = dir.sub.ptr;
while (it) |e| : (it = e.next.ptr) count += 1;
self.entries.ensureUnusedCapacity(count) catch unreachable;
it = dir.sub;
while (it) |e| : (it = e.next)
it = dir.sub.ptr;
while (it) |e| : (it = e.next.ptr)
self.entries.putAssumeCapacity(e, {});
return self;
}
@ -83,8 +83,8 @@ pub const Dir = struct {
}
}
const e = model.Entry.create(t.arena.allocator(), etype, isext, name);
e.next = self.dir.sub;
self.dir.sub = e;
e.next.ptr = self.dir.sub.ptr;
self.dir.sub.ptr = e;
return e;
}
@ -136,10 +136,10 @@ pub const Dir = struct {
pub fn final(self: *Dir, parent: ?*Dir) void {
// Remove entries we've not seen
if (self.entries.count() > 0) {
var it = &self.dir.sub;
var it = &self.dir.sub.ptr;
while (it.*) |e| {
if (self.entries.getKey(e) == e) it.* = e.next
else it = &e.next;
if (self.entries.getKey(e) == e) it.* = e.next.ptr
else it = &e.next.ptr;
}
}
self.entries.deinit();

View file

@ -42,8 +42,8 @@ fn rec(ctx: *Ctx, dir: *sink.Dir, entry: *model.Entry) void {
var ndir = dir.addDir(ctx.sink, entry.name(), &ctx.stat);
ctx.sink.setDir(ndir);
if (d.pack.err) ndir.setReadError(ctx.sink);
var it = d.sub;
while (it) |e| : (it = e.next) rec(ctx, ndir, e);
var it = d.sub.ptr;
while (it) |e| : (it = e.next.ptr) rec(ctx, ndir, e);
ctx.sink.setDir(dir);
ndir.unref(ctx.sink);
},
@ -65,8 +65,8 @@ pub fn run(d: *model.Dir) void {
const root = sink.createRoot(buf.items, &ctx.stat);
buf.deinit();
var it = d.sub;
while (it) |e| : (it = e.next) rec(&ctx, root, e);
var it = d.sub.ptr;
while (it) |e| : (it = e.next.ptr) rec(&ctx, root, e);
root.unref(ctx.sink);
sink.done();

View file

@ -37,6 +37,20 @@ pub const EType = enum(i3) {
// Type for the Entry.Packed.blocks field. Smaller than a u64 to make room for flags.
pub const Blocks = u60;
// Entries read from bin_reader may refer to other entries by itemref rather than pointer.
// This is a hack that allows browser.zig to use the same types for in-memory
// and bin_reader-backed directory trees. Most code can only deal with
// in-memory trees and accesses the .ptr field directly.
pub const Ref = extern union {
ptr: ?*Entry align(1),
ref: u64 align(1),
pub fn isNull(r: Ref) bool {
if (main.config.binreader) return r.ref == std.math.maxInt(u64)
else return r.ptr == null;
}
};
// Memory layout:
// (Ext +) Dir + name
// or: (Ext +) Link + name
@ -51,7 +65,7 @@ pub const Blocks = u60;
pub const Entry = extern struct {
pack: Packed align(1),
size: u64 align(1) = 0,
next: ?*Entry align(1) = null,
next: Ref = .{ .ptr = null },
pub const Packed = packed struct(u64) {
etype: EType,
@ -83,6 +97,10 @@ pub const Entry = extern struct {
return std.mem.sliceTo(name_ptr, 0);
}
pub fn nameHash(self: *const Self) u64 {
return std.hash.Wyhash.hash(0, self.name());
}
pub fn ext(self: *Self) ?*Ext {
if (!self.pack.isext) return null;
return @ptrCast(@as([*]Ext, @ptrCast(self)) - 1);
@ -115,6 +133,17 @@ pub const Entry = extern struct {
};
}
pub fn destroy(self: *Self, allocator: std.mem.Allocator) void {
const ptr: [*]u8 = if (self.ext()) |e| @ptrCast(e) else @ptrCast(self);
const esize: usize = switch (self.pack.etype) {
.dir => @sizeOf(Dir),
.link => @sizeOf(Link),
else => @sizeOf(File),
};
const size = (if (self.pack.isext) @as(usize, @sizeOf(Ext)) else 0) + esize + self.name().len + 1;
allocator.free(ptr[0..size]);
}
fn hasErr(self: *Self) bool {
return
if(self.dir()) |d| d.pack.err or d.pack.suberr
@ -123,8 +152,8 @@ pub const Entry = extern struct {
fn removeLinks(self: *Entry) void {
if (self.dir()) |d| {
var it = d.sub;
while (it) |e| : (it = e.next) e.removeLinks();
var it = d.sub.ptr;
while (it) |e| : (it = e.next.ptr) e.removeLinks();
}
if (self.link()) |l| l.removeLink();
}
@ -136,8 +165,8 @@ pub const Entry = extern struct {
d.items = 0;
d.pack.err = false;
d.pack.suberr = false;
var it = d.sub;
while (it) |e| : (it = e.next) e.zeroStatsRec();
var it = d.sub.ptr;
while (it) |e| : (it = e.next.ptr) e.zeroStatsRec();
}
}
@ -163,7 +192,7 @@ const DevId = u30; // Can be reduced to make room for more flags in Dir.Packed.
pub const Dir = extern struct {
entry: Entry,
sub: ?*Entry align(1) = null,
sub: Ref = .{ .ptr = null },
parent: ?*Dir align(1) = null,
// entry.{blocks,size}: Total size of all unique files + dirs. Non-shared hardlinks are counted only once.
@ -210,8 +239,8 @@ pub const Dir = extern struct {
// been updated and does not propagate to parents.
pub fn updateSubErr(self: *@This()) void {
self.pack.suberr = false;
var sub = self.sub;
while (sub) |e| : (sub = e.next) {
var sub = self.sub.ptr;
while (sub) |e| : (sub = e.next.ptr) {
if (e.hasErr()) {
self.pack.suberr = true;
break;
@ -460,9 +489,8 @@ pub var root: *Dir = undefined;
test "entry" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var e = Entry.create(arena.allocator(), .reg, false, "hello");
var e = Entry.create(std.testing.allocator, .reg, false, "hello");
defer e.destroy(std.testing.allocator);
try std.testing.expectEqual(e.pack.etype, .reg);
try std.testing.expect(!e.pack.isext);
try std.testing.expectEqualStrings(e.name(), "hello");