Newer
Older
// SPDX-FileCopyrightText: 2021-2022 Yoran Heling <projects@yorhel.nl>
const std = @import("std");
const main = @import("main.zig");
const model = @import("model.zig");
const c_statfs = @cImport(@cInclude("sys/vfs.h"));
const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
// Concise stat struct for fields we're interested in, with the types used by the model.
const Stat = struct {
hlinkc: bool = false,
dir: bool = false,
reg: bool = true,
symlink: bool = false,
ext: model.Ext = .{},
fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
return util.castClamp(std.meta.fieldInfo(T, field).field_type, x);
fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
return util.castTruncate(std.meta.fieldInfo(T, field).field_type, x);
fn read(parent: std.fs.Dir, name: [:0]const u8, follow: bool) !Stat {
const stat = try std.os.fstatatZ(parent.fd, name, if (follow) 0 else std.os.AT.SYMLINK_NOFOLLOW);
return Stat{
.blocks = clamp(Stat, .blocks, stat.blocks),
.size = clamp(Stat, .size, stat.size),
.dev = truncate(Stat, .dev, stat.dev),
.ino = truncate(Stat, .ino, stat.ino),
.nlink = clamp(Stat, .nlink, stat.nlink),
.hlinkc = stat.nlink > 1 and !std.os.system.S.ISDIR(stat.mode),
.dir = std.os.system.S.ISDIR(stat.mode),
.reg = std.os.system.S.ISREG(stat.mode),
.symlink = std.os.system.S.ISLNK(stat.mode),
.ext = .{
.mtime = clamp(model.Ext, .mtime, stat.mtime().tv_sec),
.uid = truncate(model.Ext, .uid, stat.uid),
.gid = truncate(model.Ext, .gid, stat.gid),
.mode = truncate(model.Ext, .mode, stat.mode),
},
};
}
};
var kernfs_cache: std.AutoHashMap(u64,bool) = std.AutoHashMap(u64,bool).init(main.allocator);
// This function only works on Linux
fn isKernfs(dir: std.fs.Dir, dev: u64) bool {
if (kernfs_cache.get(dev)) |e| return e;
var buf: c_statfs.struct_statfs = undefined;
if (c_statfs.fstatfs(dir.fd, &buf) != 0) return false; // silently ignoring errors isn't too nice.
const iskern = switch (buf.f_type) {
// These numbers are documented in the Linux 'statfs(2)' man page, so I assume they're stable.
0x42494e4d, // BINFMTFS_MAGIC
0xcafe4a11, // BPF_FS_MAGIC
0x27e0eb, // CGROUP_SUPER_MAGIC
0x63677270, // CGROUP2_SUPER_MAGIC
0x64626720, // DEBUGFS_MAGIC
0x1cd1, // DEVPTS_SUPER_MAGIC
0x9fa0, // PROC_SUPER_MAGIC
0x6165676c, // PSTOREFS_MAGIC
0x73636673, // SECURITYFS_MAGIC
0xf97cff8c, // SELINUX_MAGIC
0x62656572, // SYSFS_MAGIC
0x74726163 // TRACEFS_MAGIC
=> true,
else => false,
kernfs_cache.put(dev, iskern) catch {};
return iskern;
// Output a JSON string.
// Could use std.json.stringify(), but that implementation is "correct" in that
// it refuses to encode non-UTF8 slices as strings. Ncdu dumps aren't valid
// JSON if we have non-UTF8 filenames, such is life...
fn writeJsonString(wr: anytype, s: []const u8) !void {
try wr.writeByte('"');
for (s) |ch| {
switch (ch) {
'\n' => try wr.writeAll("\\n"),
'\r' => try wr.writeAll("\\r"),
0x8 => try wr.writeAll("\\b"),
'\t' => try wr.writeAll("\\t"),
0xC => try wr.writeAll("\\f"),
'\\' => try wr.writeAll("\\\\"),
'"' => try wr.writeAll("\\\""),
0...7, 0xB, 0xE...0x1F, 127 => try wr.print("\\u00{x:02}", .{ch}),
else => try wr.writeByte(ch)
}
}
try wr.writeByte('"');
}
// A ScanDir represents an in-memory directory listing (i.e. model.Dir) where
// entries read from disk can be merged into, without doing an O(1) lookup for
// each entry.
const ScanDir = struct {
dir: *model.Dir,
// Lookup table for name -> *entry.
// null is never stored in the table, but instead used pass a name string
// as out-of-band argument for lookups.
entries: Map,
const Map = std.HashMap(?*model.Entry, void, HashContext, 80);
const HashContext = struct {
cmp: []const u8 = "",
pub fn hash(self: @This(), v: ?*model.Entry) u64 {
return std.hash.Wyhash.hash(0, if (v) |e| @as([]const u8, e.name()) else self.cmp);
}
pub fn eql(self: @This(), ap: ?*model.Entry, bp: ?*model.Entry) bool {
if (ap == bp) return true;
const a = if (ap) |e| @as([]const u8, e.name()) else self.cmp;
const b = if (bp) |e| @as([]const u8, e.name()) else self.cmp;
return std.mem.eql(u8, a, b);
}
};
const Self = @This();
fn init(dir: *model.Dir) Self {
var self = Self{
.dir = dir,
.entries = Map.initContext(main.allocator, HashContext{}),
};
var it = dir.sub;
self.entries.ensureUnusedCapacity(count) catch unreachable;
it = dir.sub;
while (it) |e| : (it = e.next)
self.entries.putAssumeCapacity(e, @as(void,undefined));
return self;
}
fn addSpecial(self: *Self, name: []const u8, t: Context.Special) void {
var e = blk: {
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
// XXX: If the type doesn't match, we could always do an
// 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) {
e.delStats(self.dir);
e.addStats(self.dir, 0);
}
e.file().?.resetFlags();
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(self.dir);
}
var e = model.Entry.create(.file, false, name);
e.next = self.dir.sub;
self.dir.sub = e;
e.addStats(self.dir, 0);
break :blk e;
};
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,
}
}
fn addStat(self: *Self, name: []const u8, stat: *Stat) *model.Entry {
const etype = if (stat.dir) model.EType.dir
else if (stat.hlinkc) model.EType.link
else model.EType.file;
var e = blk: {
if (self.entries.getEntryAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name })) |entry| {
// 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 sameino = if (e.link()) |l| l.ino == stat.ino else true;
if (e.etype == etype and samedev and sameino) {
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
break :blk e;
} else e.delStatsRec(self.dir);
}
var e = model.Entry.create(etype, main.config.extended, name);
e.next = self.dir.sub;
self.dir.sub = e;
break :blk e;
};
// Ignore the new size/blocks field for directories, as we don't know
// what the original values were without calling delStats() on the
// 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)) {
e.delStats(self.dir);
e.blocks = stat.blocks;
e.size = stat.size;
}
if (e.dir()) |d| {
d.parent = self.dir;
d.dev = model.devices.getId(stat.dev);
}
if (e.file()) |f| {
f.resetFlags();
f.notreg = !stat.dir and !stat.reg;
}
if (e.link()) |l| l.ino = stat.ino;
if (e.ext()) |ext| {
if (ext.mtime > stat.ext.mtime)
stat.ext.mtime = ext.mtime;
ext.* = stat.ext;
}
e.addStats(self.dir, stat.nlink);
fn final(self: *Self) void {
if (self.entries.count() == 0) // optimization for the common case
return;
var it = &self.dir.sub;
while (it.*) |e| {
if (self.entries.contains(e)) {
e.delStatsRec(self.dir);
it.* = e.next;
} else
it = &e.next;
}
}
fn deinit(self: *Self) void {
self.entries.deinit();
}
};
// Scan/import context. Entries are added in roughly the following way:
//
// ctx.pushPath(name)
// ctx.stat = ..;
// ctx.addSpecial() or ctx.addStat()
// // repeat top-level steps for files in dir, recursively.
// }
// ctx.popPath();
//
path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator),
path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator),
// 0-terminated name of the top entry, points into 'path', invalid after popPath().
// This is a workaround to Zig's directory iterator not returning a [:0]const u8.
name: [:0]const u8 = undefined,
const Writer = std.io.BufferedWriter(4096, std.fs.File.Writer);
fn writeErr(e: anyerror) noreturn {
ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) });
}
var buf = main.allocator.create(Writer) catch unreachable;
errdefer main.allocator.destroy(buf);
buf.* = std.io.bufferedWriter(out.writer());
var wr = buf.writer();
wr.writeAll("[1,2,{\"progname\":\"ncdu\",\"progver\":\"" ++ main.program_version ++ "\",\"timestamp\":") catch |e| writeErr(e);
wr.print("{d}", .{std.time.timestamp()}) catch |e| writeErr(e);
wr.writeByte('}') catch |e| writeErr(e);
var self = main.allocator.create(Self) catch unreachable;
self.* = .{ .wr = buf };
return self;
fn initMem(dir: ?*model.Dir) *Self {
var self = main.allocator.create(Self) catch unreachable;
self.* = .{ .parents = std.ArrayList(ScanDir).init(main.allocator) };
if (dir) |d| self.parents.?.append(ScanDir.init(d)) catch unreachable;
if (self.parents) |_| model.inodes.addAllStats();
wr.writer().writeByte(']') catch |e| writeErr(e);
wr.flush() catch |e| writeErr(e);
// Add the name of the file/dir entry we're currently inspecting
fn pushPath(self: *Self, name: []const u8) void {
self.path_indices.append(self.path.items.len) catch unreachable;
if (self.path.items.len > 1) self.path.append('/') catch unreachable;
const start = self.path.items.len;
self.name = self.path.items[start..self.path.items.len-1:0];
self.path.items.len -= 1;
}
fn popPath(self: *Self) void {
if (p.items.len > 0) {
var d = p.pop();
d.final();
d.deinit();
}
if (self.wr) |w| w.writer().writeByte(']') catch |e| writeErr(e);
} else
self.stat.dir = true; // repeated popPath()s mean we're closing parent dirs.
// Set a flag to indicate that there was an error listing file entries in the current directory.
// (Such errors are silently ignored when exporting to a file, as the directory metadata has already been written)
fn setDirlistError(self: *Self) void {
if (self.parents) |*p| p.items[p.items.len-1].dir.entry.setErr(p.items[p.items.len-1].dir);
}
const Special = enum { err, other_fs, kernfs, excluded };
fn writeSpecial(self: *Self, w: anytype, t: Special) !void {
try w.writeAll(",\n");
if (self.stat.dir) try w.writeByte('[');
try w.writeAll("{\"name\":");
try writeJsonString(w, self.name);
switch (t) {
.err => try w.writeAll(",\"read_error\":true"),
.other_fs => try w.writeAll(",\"excluded\":\"othfs\""),
.kernfs => try w.writeAll(",\"excluded\":\"kernfs\""),
.excluded => try w.writeAll(",\"excluded\":\"pattern\""),
}
try w.writeByte('}');
if (self.stat.dir) try w.writeByte(']');
}
// Insert the current path as a special entry (i.e. a file/dir that is not counted)
fn addSpecial(self: *Self, t: Special) void {
std.debug.assert(self.items_seen > 0); // root item can't be a special
if (t == .err) {
if (self.last_error) |p| main.allocator.free(p);
self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
p.items[p.items.len-1].addSpecial(self.name, t)
self.writeSpecial(wr.writer(), t) catch |e| writeErr(e);
self.stat.dir = false; // So that popPath() doesn't consider this as leaving a dir.
fn writeStat(self: *Self, w: anytype, dir_dev: u64) !void {
try w.writeAll(",\n");
if (self.stat.dir) try w.writeByte('[');
try w.writeAll("{\"name\":");
try writeJsonString(w, self.name);
if (self.stat.size > 0) try w.print(",\"asize\":{d}", .{ self.stat.size });
if (self.stat.blocks > 0) try w.print(",\"dsize\":{d}", .{ util.blocksToSize(self.stat.blocks) });
if (self.stat.dir and self.stat.dev != dir_dev) try w.print(",\"dev\":{d}", .{ self.stat.dev });
if (self.stat.hlinkc) try w.print(",\"ino\":{d},\"hlnkc\":true,\"nlink\":{d}", .{ self.stat.ino, self.stat.nlink });
if (!self.stat.dir and !self.stat.reg) try w.writeAll(",\"notreg\":true");
if (main.config.extended)
try w.print(",\"uid\":{d},\"gid\":{d},\"mode\":{d},\"mtime\":{d}",
.{ self.stat.ext.uid, self.stat.ext.gid, self.stat.ext.mode, self.stat.ext.mtime });
try w.writeByte('}');
}
// Insert current path as a counted file/dir/hardlink, with information from self.stat
fn addStat(self: *Self, dir_dev: u64) void {
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.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);
break :blk e;
} else
p.items[p.items.len-1].addStat(self.name, &self.stat);
if (e.dir()) |d| // Enter the directory
p.append(ScanDir.init(d)) catch unreachable;
} else if (self.wr) |wr|
self.writeStat(wr.writer(), dir_dev) catch |e| writeErr(e);
fn deinit(self: *Self) void {
if (self.last_error) |p| main.allocator.free(p);
if (self.parents) |*p| {
for (p.items) |*i| i.deinit();
p.deinit();
}
self.path.deinit();
self.path_indices.deinit();
// Context that is currently being used for scanning.
fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
var it = main.allocator.create(std.fs.Dir.Iterator) catch unreachable;
defer main.allocator.destroy(it);
it.* = dir.iterate();
while(true) {
const entry = it.next() catch {
return;
} orelse break;
// XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
const excluded = blk: {
for (main.config.exclude_patterns.items) |pat| {
while (path.len > 0) {
if (c_fnmatch.fnmatch(pat, path, 0) == 0) break :blk true;
if (std.mem.indexOfScalar(u8, path, '/')) |idx| path = path[idx+1..:0]
else break;
}
}
break :blk false;
};
if (excluded) {
continue;
};
if (main.config.same_fs and ctx.stat.dev != dir_dev) {
continue;
}
if (main.config.follow_symlinks and ctx.stat.symlink) {
if (Stat.read(dir, ctx.name, true)) |nstat| {
if (!nstat.dir) {
// Symlink targets may reside on different filesystems,
// this will break hardlink detection and counting so let's disable it.
if (ctx.stat.hlinkc and ctx.stat.dev != dir_dev)
ctx.stat.hlinkc = false;
}
} else |_| {}
}
var edir =
if (ctx.stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
continue;
} else null;
defer if (edir != null) edir.?.close();
if (@import("builtin").os.tag == .linux and main.config.exclude_kernfs and ctx.stat.dir and isKernfs(edir.?, ctx.stat.dev)) {
if (edir.?.openFileZ("CACHEDIR.TAG", .{})) |f| {
const sig = "Signature: 8a477f597d28d172789f06886806bc55";
var buf: [sig.len]u8 = undefined;
if (f.reader().readAll(&buf)) |len| {
if (len == sig.len and std.mem.eql(u8, &buf, sig)) {
continue;
}
} else |_| {}
} else |_| {}
}
ctx.addStat(dir_dev);
if (ctx.stat.dir) scanDir(ctx, edir.?, ctx.stat.dev);
pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
active_context = if (out) |f| Context.initFile(f) else Context.initMem(null);
const full_path = std.fs.realpathAlloc(main.allocator, path) catch null;
defer if (full_path) |p| main.allocator.free(p);
active_context.stat = try Stat.read(std.fs.cwd(), active_context.pathZ(), true);
if (!active_context.stat.dir) return error.NotDir;
active_context.addStat(0);
scan();
}
pub fn setupRefresh(parent: *model.Dir) void {
active_context = Context.initMem(parent);
var full_path = std.ArrayList(u8).init(main.allocator);
defer full_path.deinit();
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];
}
// To be called after setupRefresh() (or from scanRoot())
pub fn scan() void {
defer active_context.deinit();
var dir = std.fs.cwd().openDirZ(active_context.pathZ(), .{ .access_sub_paths = true, .iterate = true }) catch |e| {
active_context.last_error = main.allocator.dupeZ(u8, active_context.path.items) catch unreachable;
active_context.fatal_error = e;
while (main.state == .refresh or main.state == .scan)
main.handleEvent(true, true);
return;
};
scanDir(active_context, dir, active_context.stat.dev);
active_context.popPath();
active_context.final();
// Using a custom recursive descent JSON parser here. std.json is great, but
// has two major downsides:
// - It does strict UTF-8 validation. Which is great in general, but not so
// much for ncdu dumps that may contain non-UTF-8 paths encoded as strings.
// - The streaming parser requires complex and overly large buffering in order
// to read strings, which doesn't work so well in our case.
//
// TODO: This code isn't very elegant and is likely contains bugs. It may be
// worth factoring out the JSON parts into a separate abstraction for which
// tests can be written.
const Import = struct {
rd: std.fs.File,
rdoff: usize = 0,
rdsize: usize = 0,
rdbuf: [8*1024]u8 = undefined,
ch: u8 = 0, // last read character, 0 = EOF (or invalid null byte, who cares)
byte: u64 = 1,
line: u64 = 1,
namebuf: [32*1024]u8 = undefined,
const Self = @This();
fn die(self: *Self, str: []const u8) noreturn {
ui.die("Error importing file on line {}:{}: {s}.\n", .{ self.line, self.byte, str });
// Advance to the next byte, sets ch.
fn con(self: *Self) void {
if (self.rdoff >= self.rdsize) {
self.rdoff = 0;
self.rdsize = self.rd.read(&self.rdbuf) catch |e| switch (e) {
error.InputOutput => self.die("I/O error"),
error.IsDir => self.die("not a file"), // should be detected at open() time, but no flag for that...
error.SystemResources => self.die("out of memory"),
else => unreachable,
};
if (self.rdsize == 0) {
self.ch = 0;
return;
}
}
self.ch = self.rdbuf[self.rdoff];
self.rdoff += 1;
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
self.byte += 1;
}
// Advance to the next non-whitespace byte.
fn conws(self: *Self) void {
while (true) {
switch (self.ch) {
'\n' => {
self.line += 1;
self.byte = 1;
},
' ', '\t', '\r' => {},
else => break,
}
self.con();
}
}
// Returns the current byte and advances to the next.
fn next(self: *Self) u8 {
defer self.con();
return self.ch;
}
fn hexdig(self: *Self) u16 {
return switch (self.ch) {
'0'...'9' => self.next() - '0',
'a'...'f' => self.next() - 'a' + 10,
'A'...'F' => self.next() - 'A' + 10,
else => self.die("invalid hex digit"),
};
}
// Read a string into buf.
// Any characters beyond the size of the buffer are consumed but otherwise discarded.
// (May store fewer characters in the case of \u escapes, it's not super precise)
fn string(self: *Self, buf: []u8) []u8 {
if (self.next() != '"') self.die("expected '\"'");
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
while (true) {
const ch = self.next();
switch (ch) {
'"' => break,
'\\' => switch (self.next()) {
'"' => if (n < buf.len) { buf[n] = '"'; n += 1; },
'\\'=> if (n < buf.len) { buf[n] = '\\';n += 1; },
'/' => if (n < buf.len) { buf[n] = '/'; n += 1; },
'b' => if (n < buf.len) { buf[n] = 0x8; n += 1; },
'f' => if (n < buf.len) { buf[n] = 0xc; n += 1; },
'n' => if (n < buf.len) { buf[n] = 0xa; n += 1; },
'r' => if (n < buf.len) { buf[n] = 0xd; n += 1; },
't' => if (n < buf.len) { buf[n] = 0x9; n += 1; },
'u' => {
const char = (self.hexdig()<<12) + (self.hexdig()<<8) + (self.hexdig()<<4) + self.hexdig();
if (n + 6 < buf.len)
n += std.unicode.utf8Encode(char, buf[n..n+5]) catch unreachable;
},
else => self.die("invalid escape sequence"),
},
0x20, 0x21, 0x23...0x5b, 0x5d...0xff => if (n < buf.len) { buf[n] = ch; n += 1; },
else => self.die("invalid character in string"),
}
}
return buf[0..n];
}
fn uint(self: *Self, T: anytype) T {
if (self.ch == '0') {
self.con();
return 0;
}
var v: T = 0;
while (self.ch >= '0' and self.ch <= '9') {
const newv = v *% 10 +% (self.ch - '0');
if (newv < v) self.die("integer out of range");
v = newv;
self.con();
}
if (v == 0) self.die("expected number");
return v;
}
fn boolean(self: *Self) bool {
switch (self.next()) {
't' => {
if (self.next() == 'r' and self.next() == 'u' and self.next() == 'e')
return true;
},
'f' => {
if (self.next() == 'a' and self.next() == 'l' and self.next() == 's' and self.next() == 'e')
return false;
},
else => {}
}
self.die("expected boolean");
}
// Consume and discard any JSON value.
fn conval(self: *Self) void {
switch (self.ch) {
't' => _ = self.boolean(),
'f' => _ = self.boolean(),
'n' => {
self.con();
if (!(self.next() == 'u' and self.next() == 'l' and self.next() == 'l'))
self.die("invalid JSON value");
},
'"' => _ = self.string(&[0]u8{}),
'{' => {
self.con();
self.conws();
if (self.ch == '}') { self.con(); return; }
while (true) {
self.conws();
_ = self.string(&[0]u8{});
self.conws();
if (self.next() != ':') self.die("expected ':'");
self.conws();
self.conval();
self.conws();
switch (self.next()) {
',' => continue,
'}' => break,
else => self.die("expected ',' or '}'"),
}
}
},
'[' => {
self.con();
self.conws();
if (self.ch == ']') { self.con(); return; }
while (true) {
self.conws();
self.conval();
self.conws();
switch (self.next()) {
',' => continue,
']' => break,
else => self.die("expected ',' or ']'"),
}
}
},
'-', '0'...'9' => {
self.con();
// Numbers are kind of annoying, this "parsing" is invalid and ultra-lazy.
while (true) {
switch (self.ch) {
'-', '+', 'e', 'E', '.', '0'...'9' => self.con(),
else => return,
}
}
},
else => self.die("invalid JSON value"),
}
fn itemkey(self: *Self, key: []const u8, name: *?[]u8, special: *?Context.Special) void {
const eq = std.mem.eql;
switch (if (key.len > 0) key[0] else @as(u8,0)) {
'a' => {
if (eq(u8, key, "asize")) {
self.ctx.stat.size = self.uint(u64);
return;
}
},
'd' => {
if (eq(u8, key, "dsize")) {
self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9);
return;
}
if (eq(u8, key, "dev")) {
self.ctx.stat.dev = self.uint(u64);
return;
}
},
'e' => {
if (eq(u8, key, "excluded")) {
var buf: [32]u8 = undefined;
const typ = self.string(&buf);
// "frmlnk" is also possible, but currently considered equivalent to "pattern".
if (eq(u8, typ, "otherfs")) special.* = .other_fs
else if (eq(u8, typ, "kernfs")) special.* = .kernfs
else special.* = .excluded;
}
},
'g' => {
if (eq(u8, key, "gid")) {
self.ctx.stat.ext.gid = self.uint(u32);
return;
}
},
'h' => {
if (eq(u8, key, "hlnkc")) {
self.ctx.stat.hlinkc = self.boolean();
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
return;
}
},
'i' => {
if (eq(u8, key, "ino")) {
self.ctx.stat.ino = self.uint(u64);
return;
}
},
'm' => {
if (eq(u8, key, "mode")) {
self.ctx.stat.ext.mode = self.uint(u16);
return;
}
if (eq(u8, key, "mtime")) {
self.ctx.stat.ext.mtime = self.uint(u64);
// Accept decimal numbers, but discard the fractional part because our data model doesn't support it.
if (self.ch == '.') {
self.con();
while (self.ch >= '0' and self.ch <= '9')
self.con();
}
return;
}
},
'n' => {
if (eq(u8, key, "name")) {
if (name.* != null) self.die("duplicate key");
name.* = self.string(&self.namebuf);
if (name.*.?.len > self.namebuf.len-5) self.die("too long file name");
return;
}
if (eq(u8, key, "nlink")) {
self.ctx.stat.nlink = self.uint(u31);
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
if (!self.ctx.stat.dir and self.ctx.stat.nlink > 1)
self.ctx.stat.hlinkc = true;
return;
}
if (eq(u8, key, "notreg")) {
self.ctx.stat.reg = !self.boolean();
return;
}
},
'r' => {
if (eq(u8, key, "read_error")) {
if (self.boolean())
special.* = .err;
return;
}
},
'u' => {
if (eq(u8, key, "uid")) {
self.ctx.stat.ext.uid = self.uint(u32);
return;
}
},
else => {},
}
self.conval();
}
fn iteminfo(self: *Self, dir_dev: u64) void {
if (self.next() != '{') self.die("expected '{'");
self.ctx.stat.dev = dir_dev;
var name: ?[]u8 = null;
var special: ?Context.Special = null;
while (true) {
self.conws();
var keybuf: [32]u8 = undefined;
const key = self.string(&keybuf);
self.conws();
if (self.next() != ':') self.die("expected ':'");
self.conws();
self.itemkey(key, &name, &special);
self.conws();
switch (self.next()) {
',' => continue,
'}' => break,
else => self.die("expected ',' or '}'"),
}
}
if (special) |s| self.ctx.addSpecial(s)
else self.ctx.addStat(dir_dev);
}
fn item(self: *Self, dev: u64) void {
self.ctx.stat = .{};
self.ctx.stat.dir = true;
self.con();
self.conws();
}
self.iteminfo(dev);
self.conws();
const ndev = self.ctx.stat.dev;
while (self.ch == ',') {
self.con();
self.conws();
self.item(ndev);
self.conws();
}
if (self.next() != ']') self.die("expected ',' or ']'");
}
self.ctx.popPath();
if ((self.ctx.items_seen & 1023) == 0)
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
}
fn root(self: *Self) void {
self.con();
self.conws();
if (self.next() != '[') self.die("expected '['");
self.conws();
if (self.uint(u16) != 1) self.die("incompatible major format version");
self.conws();
if (self.next() != ',') self.die("expected ','");
self.conws();
_ = self.uint(u16); // minor version, ignored for now
self.conws();
if (self.next() != ',') self.die("expected ','");
self.conws();
// metadata object
if (self.ch != '{') self.die("expected '{'");
self.conval(); // completely discarded
self.conws();
if (self.next() != ',') self.die("expected ','");
self.conws();
// root element
if (self.ch != '[') self.die("expected '['"); // top-level entry must be a dir
self.item(0);
self.conws();
// any trailing elements
while (self.ch == ',') {
self.con();
self.conws();
self.conval();
self.conws();
}
if (self.next() != ']') self.die("expected ',' or ']'");
self.conws();
if (self.ch != 0) self.die("trailing garbage");
}
};
pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
var fd = if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
else std.fs.cwd().openFileZ(path, .{})
catch |e| ui.die("Error reading file: {s}.\n", .{ui.errorString(e)});
active_context = if (out) |f| Context.initFile(f) else Context.initMem(null);
var imp = Import{ .ctx = active_context, .rd = fd };
const box = ui.Box.create(7, width, "Scan error");
box.move(2, 2);
ui.addstr("Path: ");
ui.addstr(ui.shorten(ui.toUtf8(active_context.last_error.?), width -| 10));
ui.addstr(ui.shorten(ui.errorString(err), width -| 6));