From 91281ef11f4a826e5e369844dc222924e580d16f Mon Sep 17 00:00:00 2001
From: Yorhel <git@yorhel.nl>
Date: Wed, 2 Nov 2022 11:28:43 +0100
Subject: [PATCH] Use extern instead of packed structs for the data model

Still using a few embedded packed structs for those fields that benefit
from bit packing. This isn't much cleaner than using packed structs for
everything, but it does have better semantics. In particular, all fields
(except those inside nested packed structs) are now guaranteed to be
byte-aligned and I don't have to worry about the memory representation
of integers when pointer-casting between the different Entry types.
---
 src/browser.zig |  46 ++++++------
 src/delete.zig  |   2 +-
 src/model.zig   | 181 ++++++++++++++++++++++++------------------------
 src/scan.zig    |  34 ++++-----
 4 files changed, 130 insertions(+), 133 deletions(-)

diff --git a/src/browser.zig b/src/browser.zig
index 13e09eb..8ec4765 100644
--- a/src/browser.zig
+++ b/src/browser.zig
@@ -83,22 +83,22 @@ fn sortLt(_: void, ap: ?*model.Entry, bp: ?*model.Entry) bool {
     switch (main.config.sort_col) {
         .name => {}, // name sorting is the fallback
         .blocks => {
-            if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+            if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
             if (sortIntLt(a.size, b.size)) |r| return r;
         },
         .size => {
             if (sortIntLt(a.size, b.size)) |r| return r;
-            if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+            if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
         },
         .items => {
             const ai = if (a.dir()) |d| d.items else 0;
             const bi = if (b.dir()) |d| d.items else 0;
             if (sortIntLt(ai, bi)) |r| return r;
-            if (sortIntLt(a.blocks, b.blocks)) |r| return r;
+            if (sortIntLt(a.pack.blocks, b.pack.blocks)) |r| return r;
             if (sortIntLt(a.size, b.size)) |r| return r;
         },
         .mtime => {
-            if (!a.isext or !b.isext) return a.isext;
+            if (!a.pack.isext or !b.pack.isext) return a.pack.isext;
             if (sortIntLt(a.ext().?.mtime, b.ext().?.mtime)) |r| return r;
         },
     }
@@ -135,10 +135,10 @@ pub fn loadDir(next_sel: ?*const model.Entry) void {
         dir_items.append(null) catch unreachable;
     var it = dir_parent.sub;
     while (it) |e| {
-        if (e.blocks > dir_max_blocks) dir_max_blocks = e.blocks;
+        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: {
-            const excl = if (e.file()) |f| f.excluded else false;
+            const excl = if (e.file()) |f| f.pack.excluded else false;
             const name = e.name();
             break :blk !excl and name[0] != '.' and name[name.len-1] != '~';
         };
@@ -164,14 +164,14 @@ const Row = struct {
         const item = self.item orelse return;
         const ch: u7 = ch: {
             if (item.file()) |f| {
-                if (f.err) break :ch '!';
-                if (f.excluded) break :ch '<';
-                if (f.other_fs) break :ch '>';
-                if (f.kernfs) break :ch '^';
-                if (f.notreg) break :ch '@';
+                if (f.pack.err) break :ch '!';
+                if (f.pack.excluded) break :ch '<';
+                if (f.pack.other_fs) break :ch '>';
+                if (f.pack.kernfs) break :ch '^';
+                if (f.pack.notreg) break :ch '@';
             } else if (item.dir()) |d| {
-                if (d.err) break :ch '!';
-                if (d.suberr) break :ch '.';
+                if (d.pack.err) break :ch '!';
+                if (d.pack.suberr) break :ch '.';
                 if (d.sub == null) break :ch 'e';
             } else if (item.link()) |_| break :ch 'H';
             return;
@@ -187,7 +187,7 @@ const Row = struct {
             width += 2 + width;
         defer self.col += width;
         const item = self.item orelse return;
-        const siz = if (main.config.show_blocks) util.blocksToSize(item.blocks) else item.size;
+        const siz = if (main.config.show_blocks) util.blocksToSize(item.pack.blocks) else item.size;
         var shr = if (item.dir()) |d| (if (main.config.show_blocks) util.blocksToSize(d.shared_blocks) else d.shared_size) else 0;
         if (main.config.show_shared == .unique) shr = siz -| shr;
 
@@ -216,8 +216,8 @@ const Row = struct {
         if (main.config.show_percent) {
             self.bg.fg(.num);
             ui.addprint("{d:>5.1}", .{ 100 *
-                if (main.config.show_blocks) @intToFloat(f32, item.blocks) / @intToFloat(f32, std.math.max(1, dir_parent.entry.blocks))
-                else                         @intToFloat(f32, item.size)   / @intToFloat(f32, std.math.max(1, dir_parent.entry.size))
+                if (main.config.show_blocks) @intToFloat(f32, item.pack.blocks) / @intToFloat(f32, std.math.max(1, dir_parent.entry.pack.blocks))
+                else                         @intToFloat(f32, item.size)        / @intToFloat(f32, std.math.max(1, dir_parent.entry.size))
             });
             self.bg.fg(.default);
             ui.addch('%');
@@ -225,7 +225,7 @@ const Row = struct {
         if (main.config.show_graph and main.config.show_percent) ui.addch(' ');
         if (main.config.show_graph) {
             var max = if (main.config.show_blocks) dir_max_blocks else dir_max_size;
-            var num = if (main.config.show_blocks) item.blocks else item.size;
+            var num = if (main.config.show_blocks) item.pack.blocks else item.size;
             if (max < bar_size) {
                 max *= bar_size;
                 num *= bar_size;
@@ -290,7 +290,7 @@ const Row = struct {
     fn name(self: *Self) void {
         ui.move(self.row, self.col);
         if (self.item) |i| {
-            self.bg.fg(if (i.etype == .dir) .dir else .default);
+            self.bg.fg(if (i.pack.etype == .dir) .dir else .default);
             ui.addch(if (i.isDirectory()) '/' else ' ');
             ui.addstr(ui.shorten(ui.toUtf8(i.name()), ui.cols -| self.col -| 1));
         } else {
@@ -460,7 +460,7 @@ const info = struct {
         } else {
             ui.addstr("Type: ");
             ui.style(.default);
-            ui.addstr(if (e.isDirectory()) "Directory" else if (if (e.file()) |f| f.notreg else false) "Other" else "File");
+            ui.addstr(if (e.isDirectory()) "Directory" else if (if (e.file()) |f| f.pack.notreg else false) "Other" else "File");
         }
         row.* += 1;
 
@@ -474,7 +474,7 @@ const info = struct {
         }
 
         // Disk usage & Apparent size
-        drawSize(box, row, "   Disk usage: ", util.blocksToSize(e.blocks), if (e.dir()) |d| util.blocksToSize(d.shared_blocks) else 0);
+        drawSize(box, row, "   Disk usage: ", util.blocksToSize(e.pack.blocks), if (e.dir()) |d| util.blocksToSize(d.shared_blocks) else 0);
         drawSize(box, row, "Apparent size: ", e.size, if (e.dir()) |d| d.shared_size else 0);
 
         // Number of items
@@ -522,7 +522,7 @@ const info = struct {
         var row: u32 = 2;
 
         // Tabs
-        if (e.etype == .link) {
+        if (e.pack.etype == .link) {
             box.tab(cols-19, tab == .info, 1, "Info");
             box.tab(cols-10, tab == .links, 2, "Links");
         }
@@ -543,7 +543,7 @@ const info = struct {
     }
 
     fn keyInput(ch: i32) bool {
-        if (entry.?.etype == .link) {
+        if (entry.?.pack.etype == .link) {
             switch (ch) {
                 '1', 'h', ui.c.KEY_LEFT => { set(entry, .info); return true; },
                 '2', 'l', ui.c.KEY_RIGHT => { set(entry, .links); return true; },
@@ -778,7 +778,7 @@ pub fn draw() void {
     ui.move(ui.rows-1, 1);
     ui.style(if (main.config.show_blocks) .bold_hd else .hd);
     ui.addstr("Total disk usage: ");
-    ui.addsize(.hd, util.blocksToSize(dir_parent.entry.blocks));
+    ui.addsize(.hd, util.blocksToSize(dir_parent.entry.pack.blocks));
     ui.style(if (main.config.show_blocks) .hd else .bold_hd);
     ui.addstr("  Apparent size: ");
     ui.addsize(.hd, dir_parent.entry.size);
diff --git a/src/delete.zig b/src/delete.zig
index 1f6575c..d98a2bd 100644
--- a/src/delete.zig
+++ b/src/delete.zig
@@ -100,7 +100,7 @@ fn drawConfirm() void {
     ui.addstr("Are you sure you want to delete \"");
     ui.addstr(ui.shorten(ui.toUtf8(entry.name()), 21));
     ui.addch('"');
-    if (entry.etype != .dir)
+    if (entry.pack.etype != .dir)
         ui.addch('?')
     else {
         box.move(2, 18);
diff --git a/src/model.zig b/src/model.zig
index 75cfe3d..cfd67b5 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -17,7 +17,7 @@ const allocator = allocator_state.allocator();
 
 pub const EType = enum(u2) { dir, link, file };
 
-// Type for the Entry.blocks field. Smaller than a u64 to make room for flags.
+// Type for the Entry.Packed.blocks field. Smaller than a u64 to make room for flags.
 pub const Blocks = u60;
 
 // Memory layout:
@@ -31,38 +31,40 @@ pub const Blocks = u60;
 // These are all packed structs and hence do not have any alignment, which is
 // great for saving memory but perhaps not very great for code size or
 // performance.
-// (TODO: What are the aliassing rules for Zig? There is a 'noalias' keyword,
-// but does that mean all unmarked pointers are allowed to alias?)
-pub const Entry = packed struct {
-    etype: EType,
-    isext: bool,
-    // Whether or not this entry's size has been counted in its parents.
-    // Counting of Link entries is deferred until the scan/delete operation has
-    // completed, so for those entries this flag indicates an intention to be
-    // counted.
-    counted: bool,
-    blocks: Blocks, // 512-byte blocks
-    size: u64,
-    next: ?*Entry,
+pub const Entry = extern struct {
+    pack: Packed align(1),
+    size: u64 align(1),
+    next: ?*Entry align(1),
+
+    pub const Packed = packed struct(u64) {
+        etype: EType,
+        isext: bool,
+        // Whether or not this entry's size has been counted in its parents.
+        // Counting of Link entries is deferred until the scan/delete operation has
+        // completed, so for those entries this flag indicates an intention to be
+        // counted.
+        counted: bool,
+        blocks: Blocks, // 512-byte blocks
+    };
 
     const Self = @This();
 
     pub fn dir(self: *Self) ?*Dir {
-        return if (self.etype == .dir) @ptrCast(*Dir, self) else null;
+        return if (self.pack.etype == .dir) @ptrCast(*Dir, self) else null;
     }
 
     pub fn link(self: *Self) ?*Link {
-        return if (self.etype == .link) @ptrCast(*Link, self) else null;
+        return if (self.pack.etype == .link) @ptrCast(*Link, self) else null;
     }
 
     pub fn file(self: *Self) ?*File {
-        return if (self.etype == .file) @ptrCast(*File, self) else null;
+        return if (self.pack.etype == .file) @ptrCast(*File, self) else null;
     }
 
     // Whether this entry should be displayed as a "directory".
     // Some dirs are actually represented in this data model as a File for efficiency.
     pub fn isDirectory(self: *Self) bool {
-        return if (self.file()) |f| f.other_fs or f.kernfs else self.etype == .dir;
+        return if (self.file()) |f| f.pack.other_fs or f.pack.kernfs else self.pack.etype == .dir;
     }
 
     fn nameOffset(etype: EType) usize {
@@ -74,12 +76,12 @@ pub const Entry = packed struct {
     }
 
     pub fn name(self: *const Self) [:0]const u8 {
-        const ptr = @ptrCast([*:0]const u8, self) + nameOffset(self.etype);
+        const ptr = @ptrCast([*:0]const u8, self) + nameOffset(self.pack.etype); // TODO: ptrCast the 'name' field instead.
         return std.mem.sliceTo(ptr, 0);
     }
 
     pub fn ext(self: *Self) ?*Ext {
-        if (!self.isext) return null;
+        if (!self.pack.isext) return null;
         return @ptrCast(*Ext, @ptrCast([*]Ext, self) - 1);
     }
 
@@ -88,7 +90,7 @@ pub const Entry = packed struct {
         const size = nameOffset(etype) + ename.len + 1 + extsize;
         var ptr = blk: {
             while (true) {
-                if (allocator.allocWithOptions(u8, size, std.math.max(@alignOf(Ext), @alignOf(Entry)), null)) |p|
+                if (allocator.allocWithOptions(u8, size, 1, null)) |p|
                     break :blk p
                 else |_| {}
                 ui.oom();
@@ -96,8 +98,8 @@ pub const Entry = packed struct {
         };
         std.mem.set(u8, ptr, 0); // kind of ugly, but does the trick
         var e = @ptrCast(*Entry, ptr.ptr + extsize);
-        e.etype = etype;
-        e.isext = isext;
+        e.pack.etype = etype;
+        e.pack.isext = isext;
         var name_ptr = @ptrCast([*]u8, e) + nameOffset(etype);
         std.mem.copy(u8, name_ptr[0..ename.len], ename);
         return e;
@@ -105,19 +107,19 @@ pub const Entry = packed struct {
 
     // Set the 'err' flag on Dirs and Files, propagating 'suberr' to parents.
     pub fn setErr(self: *Self, parent: *Dir) void {
-        if (self.dir()) |d| d.err = true
-        else if (self.file()) |f| f.err = true
+        if (self.dir()) |d| d.pack.err = true
+        else if (self.file()) |f| f.pack.err = true
         else unreachable;
         var it: ?*Dir = if (&parent.entry == self) parent.parent else parent;
         while (it) |p| : (it = p.parent) {
-            if (p.suberr) break;
-            p.suberr = true;
+            if (p.pack.suberr) break;
+            p.pack.suberr = true;
         }
     }
 
     pub fn addStats(self: *Entry, parent: *Dir, nlink: u31) void {
-        if (self.counted) return;
-        self.counted = true;
+        if (self.pack.counted) return;
+        self.pack.counted = true;
 
         // Add link to the inode map, but don't count its size (yet).
         if (self.link()) |l| {
@@ -125,7 +127,7 @@ pub const Entry = packed struct {
             var d = inodes.map.getOrPut(l) catch unreachable;
             if (!d.found_existing) {
                 d.value_ptr.* = .{ .counted = false, .nlink = nlink };
-                inodes.total_blocks +|= self.blocks;
+                inodes.total_blocks +|= self.pack.blocks;
                 l.next = l;
             } else {
                 inodes.setStats(.{ .key_ptr = d.key_ptr, .value_ptr = d.value_ptr }, false);
@@ -144,9 +146,9 @@ pub const Entry = packed struct {
                 if (p.entry.ext()) |pe|
                     if (e.mtime > pe.mtime) { pe.mtime = e.mtime; };
             p.items +|= 1;
-            if (self.etype != .link) {
+            if (self.pack.etype != .link) {
                 p.entry.size +|= self.size;
-                p.entry.blocks +|= self.blocks;
+                p.entry.pack.blocks +|= self.pack.blocks;
             }
         }
     }
@@ -165,8 +167,8 @@ pub const Entry = packed struct {
     // anymore, meaning that delStats() followed by addStats() with the same
     // data may cause information to be lost.
     pub fn delStats(self: *Entry, parent: *Dir) void {
-        if (!self.counted) return;
-        defer self.counted = false; // defer, to make sure inodes.setStats() still sees it as counted.
+        if (!self.pack.counted) return;
+        defer self.pack.counted = false; // defer, to make sure inodes.setStats() still sees it as counted.
 
         if (self.link()) |l| {
             var d = inodes.map.getEntry(l).?;
@@ -175,7 +177,7 @@ pub const Entry = packed struct {
             if (l.next == l) {
                 _ = inodes.map.remove(l);
                 _ = inodes.uncounted.remove(l);
-                inodes.total_blocks -|= self.blocks;
+                inodes.total_blocks -|= self.pack.blocks;
             } else {
                 if (d.key_ptr.* == l)
                     d.key_ptr.* = l.next;
@@ -195,9 +197,9 @@ pub const Entry = packed struct {
         var it: ?*Dir = parent;
         while(it) |p| : (it = p.parent) {
             p.items -|= 1;
-            if (self.etype != .link) {
+            if (self.pack.etype != .link) {
                 p.entry.size -|= self.size;
-                p.entry.blocks -|= self.blocks;
+                p.entry.pack.blocks -|= self.pack.blocks;
             }
         }
     }
@@ -212,32 +214,35 @@ pub const Entry = packed struct {
     }
 };
 
-const DevId = u30; // Can be reduced to make room for more flags in Dir.
+const DevId = u30; // Can be reduced to make room for more flags in Dir.Packed.
 
-pub const Dir = packed struct {
+pub const Dir = extern struct {
     entry: Entry,
 
-    sub: ?*Entry,
-    parent: ?*Dir,
+    sub: ?*Entry align(1),
+    parent: ?*Dir align(1),
 
     // entry.{blocks,size}: Total size of all unique files + dirs. Non-shared hardlinks are counted only once.
     //   (i.e. the space you'll need if you created a filesystem with only this dir)
     // shared_*: Unique hardlinks that still have references outside of this directory.
     //   (i.e. the space you won't reclaim by deleting this dir)
     // (space reclaimed by deleting a dir =~ entry. - shared_)
-    shared_blocks: u64,
-    shared_size: u64,
-    items: u32,
-
-    // Indexes into the global 'devices.list' array
-    dev: DevId,
+    shared_blocks: u64 align(1),
+    shared_size: u64 align(1),
+    items: u32 align(1),
 
-    err: bool,
-    suberr: bool,
+    pack: Packed align(1),
 
     // Only used to find the @offsetOff, the name is written at this point as a 0-terminated string.
     // (Old C habits die hard)
-    name: u8,
+    name: [0]u8,
+
+    pub const Packed = packed struct {
+        // Indexes into the global 'devices.list' array
+        dev: DevId,
+        err: bool,
+        suberr: bool,
+    };
 
     pub fn fmtPath(self: *const @This(), withRoot: bool, out: *std.ArrayList(u8)) void {
         if (!withRoot and self.parent == null) return;
@@ -259,13 +264,13 @@ pub const Dir = packed struct {
 };
 
 // File that's been hardlinked (i.e. nlink > 1)
-pub const Link = packed struct {
+pub const Link = extern struct {
     entry: Entry,
-    parent: *Dir,
-    next: *Link, // Singly circular linked list of all *Link nodes with the same dev,ino.
+    parent: *Dir align(1),
+    next: *Link align(1), // Singly circular linked list of all *Link nodes with the same dev,ino.
     // dev is inherited from the parent Dir
-    ino: u64,
-    name: u8,
+    ino: u64 align(1),
+    name: [0]u8,
 
     // Return value should be freed with main.allocator.
     pub fn path(self: @This(), withRoot: bool) [:0]const u8 {
@@ -278,40 +283,32 @@ pub const Link = packed struct {
 };
 
 // Anything that's not an (indexed) directory or hardlink. Excluded directories are also "Files".
-pub const File = packed struct {
+pub const File = extern struct {
     entry: Entry,
-
-    err: bool,
-    excluded: bool,
-    other_fs: bool,
-    kernfs: bool,
-    notreg: bool,
-    _pad: u3,
-
-    name: u8,
+    pack: Packed,
+    name: [0]u8,
+
+    pub const Packed = packed struct(u8) {
+        err: bool = false,
+        excluded: bool = false,
+        other_fs: bool = false,
+        kernfs: bool = false,
+        notreg: bool = false,
+        _pad: u3 = 0, // Make this struct "ABI sized" to allow inclusion in an extern struct
+    };
 
     pub fn resetFlags(f: *@This()) void {
-        f.err = false;
-        f.excluded = false;
-        f.other_fs = false;
-        f.kernfs = false;
-        f.notreg = false;
+        f.pack = .{};
     }
 };
 
-pub const Ext = packed struct {
-    mtime: u64 = 0,
-    uid: u32 = 0,
-    gid: u32 = 0,
-    mode: u16 = 0,
+pub const Ext = extern struct {
+    mtime: u64 align(1) = 0,
+    uid: u32 align(1) = 0,
+    gid: u32 align(1) = 0,
+    mode: u16 align(1) = 0,
 };
 
-comptime {
-    std.debug.assert(@bitOffsetOf(Dir, "name") % 8 == 0);
-    std.debug.assert(@bitOffsetOf(Link, "name") % 8 == 0);
-    std.debug.assert(@bitOffsetOf(File, "name") % 8 == 0);
-}
-
 
 // List of st_dev entries. Those are typically 64bits, but that's quite a waste
 // of space when a typical scan won't cover many unique devices.
@@ -367,13 +364,13 @@ pub const inodes = struct {
     const HashContext = struct {
         pub fn hash(_: @This(), l: *Link) u64 {
             var h = std.hash.Wyhash.init(0);
-            h.update(std.mem.asBytes(&@as(u32, l.parent.dev)));
+            h.update(std.mem.asBytes(&@as(u32, l.parent.pack.dev)));
             h.update(std.mem.asBytes(&l.ino));
             return h.final();
         }
 
         pub fn eql(_: @This(), a: *Link, b: *Link) bool {
-            return a.ino == b.ino and a.parent.dev == b.parent.dev;
+            return a.ino == b.ino and a.parent.pack.dev == b.parent.pack.dev;
         }
     };
 
@@ -399,7 +396,7 @@ pub const inodes = struct {
         defer dirs.deinit();
         var it = entry.key_ptr.*;
         while (true) {
-            if (it.entry.counted) {
+            if (it.entry.pack.counted) {
                 nlink += 1;
                 var parent: ?*Dir = it.parent;
                 while (parent) |p| : (parent = p.parent) {
@@ -419,19 +416,19 @@ pub const inodes = struct {
         var dir_iter = dirs.iterator();
         if (add) {
             while (dir_iter.next()) |de| {
-                de.key_ptr.*.entry.blocks +|= entry.key_ptr.*.entry.blocks;
-                de.key_ptr.*.entry.size   +|= entry.key_ptr.*.entry.size;
+                de.key_ptr.*.entry.pack.blocks +|= entry.key_ptr.*.entry.pack.blocks;
+                de.key_ptr.*.entry.size        +|= entry.key_ptr.*.entry.size;
                 if (de.value_ptr.* < nlink) {
-                    de.key_ptr.*.shared_blocks +|= entry.key_ptr.*.entry.blocks;
+                    de.key_ptr.*.shared_blocks +|= entry.key_ptr.*.entry.pack.blocks;
                     de.key_ptr.*.shared_size   +|= entry.key_ptr.*.entry.size;
                 }
             }
         } else {
             while (dir_iter.next()) |de| {
-                de.key_ptr.*.entry.blocks -|= entry.key_ptr.*.entry.blocks;
-                de.key_ptr.*.entry.size   -|= entry.key_ptr.*.entry.size;
+                de.key_ptr.*.entry.pack.blocks -|= entry.key_ptr.*.entry.pack.blocks;
+                de.key_ptr.*.entry.size        -|= entry.key_ptr.*.entry.size;
                 if (de.value_ptr.* < nlink) {
-                    de.key_ptr.*.shared_blocks -|= entry.key_ptr.*.entry.blocks;
+                    de.key_ptr.*.shared_blocks -|= entry.key_ptr.*.entry.pack.blocks;
                     de.key_ptr.*.shared_size   -|= entry.key_ptr.*.entry.size;
                 }
             }
@@ -458,7 +455,7 @@ pub var root: *Dir = undefined;
 
 test "entry" {
     var e = Entry.create(.file, false, "hello");
-    try std.testing.expectEqual(e.etype, .file);
-    try std.testing.expect(!e.isext);
+    try std.testing.expectEqual(e.pack.etype, .file);
+    try std.testing.expect(!e.pack.isext);
     try std.testing.expectEqualStrings(e.name(), "hello");
 }
diff --git a/src/scan.zig b/src/scan.zig
index 74c2ae5..142857c 100644
--- a/src/scan.zig
+++ b/src/scan.zig
@@ -156,11 +156,11 @@ const ScanDir = struct {
                 // in-place conversion to a File entry. That's more efficient,
                 // but also more code. I don't expect this to happen often.
                 var e = entry.key_ptr.*.?;
-                if (e.etype == .file) {
-                    if (e.size > 0 or e.blocks > 0) {
+                if (e.pack.etype == .file) {
+                    if (e.size > 0 or e.pack.blocks > 0) {
                         e.delStats(self.dir);
                         e.size = 0;
-                        e.blocks = 0;
+                        e.pack.blocks = 0;
                         e.addStats(self.dir, 0);
                     }
                     e.file().?.resetFlags();
@@ -177,9 +177,9 @@ const ScanDir = struct {
         var f = e.file().?;
         switch (t) {
             .err => e.setErr(self.dir),
-            .other_fs => f.other_fs = true,
-            .kernfs => f.kernfs = true,
-            .excluded => f.excluded = true,
+            .other_fs => f.pack.other_fs = true,
+            .kernfs => f.pack.kernfs = true,
+            .excluded => f.pack.excluded = true,
         }
     }
 
@@ -192,9 +192,9 @@ const ScanDir = struct {
                 // XXX: In-place conversion may also be possible here.
                 var e = entry.key_ptr.*.?;
                 // changes of dev/ino affect hard link counting in a way we can't simply merge.
-                const samedev = if (e.dir()) |d| d.dev == model.devices.getId(stat.dev) else true;
+                const samedev = if (e.dir()) |d| d.pack.dev == model.devices.getId(stat.dev) else true;
                 const sameino = if (e.link()) |l| l.ino == stat.ino else true;
-                if (e.etype == etype and samedev and sameino) {
+                if (e.pack.etype == etype and samedev and sameino) {
                     _ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
                     break :blk e;
                 } else e.delStatsRec(self.dir);
@@ -209,18 +209,18 @@ const ScanDir = struct {
         // entire subtree, which, in turn, would break all shared hardlink
         // sizes. The current approach may result in incorrect sizes after
         // refresh, but I expect the difference to be fairly minor.
-        if (!(e.etype == .dir and e.counted) and (e.blocks != stat.blocks or e.size != stat.size)) {
+        if (!(e.pack.etype == .dir and e.pack.counted) and (e.pack.blocks != stat.blocks or e.size != stat.size)) {
             e.delStats(self.dir);
-            e.blocks = stat.blocks;
+            e.pack.blocks = stat.blocks;
             e.size = stat.size;
         }
         if (e.dir()) |d| {
             d.parent = self.dir;
-            d.dev = model.devices.getId(stat.dev);
+            d.pack.dev = model.devices.getId(stat.dev);
         }
         if (e.file()) |f| {
             f.resetFlags();
-            f.notreg = !stat.dir and !stat.reg;
+            f.pack.notreg = !stat.dir and !stat.reg;
         }
         if (e.link()) |l| l.ino = stat.ino;
         if (e.ext()) |ext| {
@@ -415,11 +415,11 @@ const Context = struct {
             var e = if (p.items.len == 0) blk: {
                 // Root entry
                 var e = model.Entry.create(.dir, main.config.extended, self.name);
-                e.blocks = self.stat.blocks;
+                e.pack.blocks = self.stat.blocks;
                 e.size = self.stat.size;
                 if (e.ext()) |ext| ext.* = self.stat.ext;
                 model.root = e.dir().?;
-                model.root.dev = model.devices.getId(self.stat.dev);
+                model.root.pack.dev = model.devices.getId(self.stat.dev);
                 break :blk e;
             } else
                 p.items[p.items.len-1].addStat(self.name, &self.stat);
@@ -552,7 +552,7 @@ pub fn setupRefresh(parent: *model.Dir) void {
     parent.fmtPath(true, &full_path);
     active_context.pushPath(full_path.items);
     active_context.stat.dir = true;
-    active_context.stat.dev = model.devices.list.items[parent.dev];
+    active_context.stat.dev = model.devices.list.items[parent.pack.dev];
 }
 
 // To be called after setupRefresh() (or from scanRoot())
@@ -1021,7 +1021,7 @@ fn drawBox() void {
         box.move(2, 30);
         ui.addstr("size: ");
         // TODO: Should display the size of the dir-to-be-refreshed on refreshing, not the root.
-        ui.addsize(.default, util.blocksToSize(model.root.entry.blocks +| model.inodes.total_blocks));
+        ui.addsize(.default, util.blocksToSize(model.root.entry.pack.blocks +| model.inodes.total_blocks));
     }
 
     box.move(3, 2);
@@ -1088,7 +1088,7 @@ pub fn draw() void {
                     .{ ui.shorten(active_context.pathZ(), 63), active_context.items_seen }
                 ) catch return;
             } else {
-                const r = ui.FmtSize.fmt(util.blocksToSize(model.root.entry.blocks));
+                const r = ui.FmtSize.fmt(util.blocksToSize(model.root.entry.pack.blocks));
                 line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
                     .{ ui.shorten(active_context.pathZ(), 51), active_context.items_seen, r.num(), r.unit }
                 ) catch return;
-- 
GitLab