2021-07-18 01:36:05 -08:00
|
|
|
// SPDX-FileCopyrightText: 2021 Yoran Heling <projects@yorhel.nl>
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
2021-04-29 02:48:45 -08:00
|
|
|
const std = @import("std");
|
|
|
|
|
const main = @import("main.zig");
|
|
|
|
|
const model = @import("model.zig");
|
2021-05-09 10:58:17 -08:00
|
|
|
const ui = @import("ui.zig");
|
|
|
|
|
usingnamespace @import("util.zig");
|
2021-05-03 04:41:48 -08:00
|
|
|
const c_statfs = @cImport(@cInclude("sys/vfs.h"));
|
|
|
|
|
const c_fnmatch = @cImport(@cInclude("fnmatch.h"));
|
2021-04-29 02:48:45 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Concise stat struct for fields we're interested in, with the types used by the model.
|
|
|
|
|
const Stat = struct {
|
2021-07-13 03:33:38 -08:00
|
|
|
blocks: model.Blocks = 0,
|
2021-05-29 00:51:17 -08:00
|
|
|
size: u64 = 0,
|
|
|
|
|
dev: u64 = 0,
|
|
|
|
|
ino: u64 = 0,
|
2021-07-28 00:29:15 -08:00
|
|
|
nlink: u31 = 0,
|
2021-05-29 00:51:17 -08:00
|
|
|
hlinkc: bool = false,
|
|
|
|
|
dir: bool = false,
|
|
|
|
|
reg: bool = true,
|
|
|
|
|
symlink: bool = false,
|
|
|
|
|
ext: model.Ext = .{},
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
fn clamp(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
|
|
|
|
|
return castClamp(std.meta.fieldInfo(T, field).field_type, x);
|
|
|
|
|
}
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
fn truncate(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
|
|
|
|
|
return castTruncate(std.meta.fieldInfo(T, field).field_type, x);
|
|
|
|
|
}
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
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),
|
2021-05-29 00:51:17 -08:00
|
|
|
.hlinkc = stat.nlink > 1 and !std.os.system.S_ISDIR(stat.mode),
|
2021-05-03 04:41:48 -08:00
|
|
|
.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,
|
2021-04-29 02:48:45 -08:00
|
|
|
};
|
2021-05-03 04:41:48 -08:00
|
|
|
kernfs_cache.put(dev, iskern) catch {};
|
|
|
|
|
return iskern;
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// 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('"');
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
// 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 {
|
2021-07-26 04:03:08 -08:00
|
|
|
dir: *model.Dir,
|
|
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
// 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();
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
fn init(dir: *model.Dir) Self {
|
|
|
|
|
var self = Self{
|
|
|
|
|
.dir = dir,
|
|
|
|
|
.entries = Map.initContext(main.allocator, HashContext{}),
|
|
|
|
|
};
|
2021-07-13 03:33:38 -08:00
|
|
|
|
|
|
|
|
var count: Map.Size = 0;
|
2021-07-26 04:03:08 -08:00
|
|
|
var it = dir.sub;
|
2021-07-13 03:33:38 -08:00
|
|
|
while (it) |e| : (it = e.next) count += 1;
|
|
|
|
|
self.entries.ensureCapacity(count) catch unreachable;
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
it = dir.sub;
|
2021-07-13 03:33:38 -08:00
|
|
|
while (it) |e| : (it = e.next)
|
|
|
|
|
self.entries.putAssumeCapacity(e, @as(void,undefined));
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
fn addSpecial(self: *Self, name: []const u8, t: Context.Special) void {
|
2021-07-13 03:33:38 -08:00
|
|
|
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) {
|
2021-07-26 04:03:08 -08:00
|
|
|
e.delStats(self.dir);
|
2021-07-13 03:33:38 -08:00
|
|
|
e.size = 0;
|
|
|
|
|
e.blocks = 0;
|
2021-07-28 00:29:15 -08:00
|
|
|
e.addStats(self.dir, 0);
|
2021-07-13 03:33:38 -08:00
|
|
|
}
|
|
|
|
|
e.file().?.resetFlags();
|
|
|
|
|
_ = self.entries.removeAdapted(@as(?*model.Entry,null), HashContext{ .cmp = name });
|
|
|
|
|
break :blk e;
|
2021-07-26 04:03:08 -08:00
|
|
|
} else e.delStatsRec(self.dir);
|
2021-07-13 03:33:38 -08:00
|
|
|
}
|
|
|
|
|
var e = model.Entry.create(.file, false, name);
|
2021-07-26 04:03:08 -08:00
|
|
|
e.next = self.dir.sub;
|
|
|
|
|
self.dir.sub = e;
|
2021-07-28 00:29:15 -08:00
|
|
|
e.addStats(self.dir, 0);
|
2021-07-13 03:33:38 -08:00
|
|
|
break :blk e;
|
|
|
|
|
};
|
|
|
|
|
var f = e.file().?;
|
|
|
|
|
switch (t) {
|
2021-07-26 04:03:08 -08:00
|
|
|
.err => e.setErr(self.dir),
|
2021-07-13 03:33:38 -08:00
|
|
|
.other_fs => f.other_fs = true,
|
|
|
|
|
.kernfs => f.kernfs = true,
|
|
|
|
|
.excluded => f.excluded = true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
fn addStat(self: *Self, name: []const u8, stat: *Stat) *model.Entry {
|
2021-07-13 03:33:38 -08:00
|
|
|
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.*.?;
|
2021-07-28 00:29:15 -08:00
|
|
|
// changes of dev/ino affect hard link counting in a way we can't simply merge.
|
2021-07-13 03:33:38 -08:00
|
|
|
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;
|
2021-07-26 04:03:08 -08:00
|
|
|
} else e.delStatsRec(self.dir);
|
2021-07-13 03:33:38 -08:00
|
|
|
}
|
|
|
|
|
var e = model.Entry.create(etype, main.config.extended, name);
|
2021-07-26 04:03:08 -08:00
|
|
|
e.next = self.dir.sub;
|
|
|
|
|
self.dir.sub = e;
|
2021-07-13 03:33:38 -08:00
|
|
|
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.
|
2021-07-19 06:58:34 -08:00
|
|
|
if (!(e.etype == .dir and e.counted) and (e.blocks != stat.blocks or e.size != stat.size)) {
|
2021-07-26 04:03:08 -08:00
|
|
|
e.delStats(self.dir);
|
2021-07-13 03:33:38 -08:00
|
|
|
e.blocks = stat.blocks;
|
|
|
|
|
e.size = stat.size;
|
|
|
|
|
}
|
2021-07-26 04:03:08 -08:00
|
|
|
if (e.dir()) |d| {
|
|
|
|
|
d.parent = self.dir;
|
|
|
|
|
d.dev = model.devices.getId(stat.dev);
|
|
|
|
|
}
|
2021-07-13 03:33:38 -08:00
|
|
|
if (e.file()) |f| {
|
|
|
|
|
f.resetFlags();
|
|
|
|
|
f.notreg = !stat.dir and !stat.reg;
|
|
|
|
|
}
|
2021-07-28 00:29:15 -08:00
|
|
|
if (e.link()) |l| l.ino = stat.ino;
|
2021-07-13 03:33:38 -08:00
|
|
|
if (e.ext()) |ext| {
|
|
|
|
|
if (ext.mtime > stat.ext.mtime)
|
|
|
|
|
stat.ext.mtime = ext.mtime;
|
|
|
|
|
ext.* = stat.ext;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-28 00:29:15 -08:00
|
|
|
e.addStats(self.dir, stat.nlink);
|
2021-07-13 03:33:38 -08:00
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
fn final(self: *Self) void {
|
2021-07-13 03:33:38 -08:00
|
|
|
if (self.entries.count() == 0) // optimization for the common case
|
|
|
|
|
return;
|
2021-07-26 04:03:08 -08:00
|
|
|
var it = &self.dir.sub;
|
2021-07-13 03:33:38 -08:00
|
|
|
while (it.*) |e| {
|
|
|
|
|
if (self.entries.contains(e)) {
|
2021-07-26 04:03:08 -08:00
|
|
|
e.delStatsRec(self.dir);
|
2021-07-13 03:33:38 -08:00
|
|
|
it.* = e.next;
|
|
|
|
|
} else
|
|
|
|
|
it = &e.next;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn deinit(self: *Self) void {
|
|
|
|
|
self.entries.deinit();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// Scan/import context. Entries are added in roughly the following way:
|
|
|
|
|
//
|
|
|
|
|
// ctx.pushPath(name)
|
|
|
|
|
// ctx.stat = ..;
|
|
|
|
|
// ctx.addSpecial() or ctx.addStat()
|
2021-05-29 00:51:17 -08:00
|
|
|
// if (ctx.stat.dir) {
|
2021-05-12 01:04:06 -08:00
|
|
|
// // repeat top-level steps for files in dir, recursively.
|
|
|
|
|
// }
|
|
|
|
|
// ctx.popPath();
|
|
|
|
|
//
|
2021-05-03 04:41:48 -08:00
|
|
|
const Context = struct {
|
2021-05-12 01:04:06 -08:00
|
|
|
// When scanning to RAM
|
2021-07-26 04:03:08 -08:00
|
|
|
parents: ?std.ArrayList(ScanDir) = std.ArrayList(ScanDir).init(main.allocator),
|
2021-05-12 01:04:06 -08:00
|
|
|
// When scanning to a file
|
2021-05-29 00:51:17 -08:00
|
|
|
wr: ?*Writer = null,
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator),
|
|
|
|
|
path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator),
|
2021-05-29 00:51:17 -08:00
|
|
|
items_seen: u32 = 0,
|
2021-05-03 04:41:48 -08:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
|
2021-05-09 10:58:17 -08:00
|
|
|
last_error: ?[:0]u8 = null,
|
2021-07-13 03:33:38 -08:00
|
|
|
fatal_error: ?anyerror = null,
|
2021-05-09 10:58:17 -08:00
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
stat: Stat = undefined,
|
|
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
const Writer = std.io.BufferedWriter(4096, std.fs.File.Writer);
|
2021-05-03 04:41:48 -08:00
|
|
|
const Self = @This();
|
|
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
fn writeErr(e: anyerror) noreturn {
|
|
|
|
|
ui.die("Error writing to file: {s}.\n", .{ ui.errorString(e) });
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
fn initFile(out: std.fs.File) *Self {
|
2021-05-29 03:18:21 -08:00
|
|
|
var buf = main.allocator.create(Writer) catch unreachable;
|
2021-05-29 00:51:17 -08:00
|
|
|
errdefer main.allocator.destroy(buf);
|
|
|
|
|
buf.* = std.io.bufferedWriter(out.writer());
|
|
|
|
|
var wr = buf.writer();
|
2021-05-29 09:22:00 -08:00
|
|
|
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);
|
2021-07-13 03:33:38 -08:00
|
|
|
|
|
|
|
|
var self = main.allocator.create(Self) catch unreachable;
|
|
|
|
|
self.* = .{ .wr = buf };
|
|
|
|
|
return self;
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
fn initMem(dir: ?*model.Dir) *Self {
|
2021-07-13 03:33:38 -08:00
|
|
|
var self = main.allocator.create(Self) catch unreachable;
|
2021-07-26 04:03:08 -08:00
|
|
|
self.* = .{ .parents = std.ArrayList(ScanDir).init(main.allocator) };
|
|
|
|
|
if (dir) |d| self.parents.?.append(ScanDir.init(d)) catch unreachable;
|
2021-07-13 03:33:38 -08:00
|
|
|
return self;
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
fn final(self: *Self) void {
|
2021-07-28 00:29:15 -08:00
|
|
|
if (self.parents) |_| model.inodes.addAllStats();
|
2021-05-29 00:51:17 -08:00
|
|
|
if (self.wr) |wr| {
|
2021-05-29 09:22:00 -08:00
|
|
|
wr.writer().writeByte(']') catch |e| writeErr(e);
|
|
|
|
|
wr.flush() catch |e| writeErr(e);
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// Add the name of the file/dir entry we're currently inspecting
|
2021-05-29 03:18:21 -08:00
|
|
|
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;
|
2021-05-03 04:41:48 -08:00
|
|
|
const start = self.path.items.len;
|
2021-05-29 03:18:21 -08:00
|
|
|
self.path.appendSlice(name) catch unreachable;
|
2021-05-03 04:41:48 -08:00
|
|
|
|
2021-05-29 03:18:21 -08:00
|
|
|
self.path.append(0) catch unreachable;
|
2021-05-03 04:41:48 -08:00
|
|
|
self.name = self.path.items[start..self.path.items.len-1:0];
|
|
|
|
|
self.path.items.len -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn popPath(self: *Self) void {
|
2021-07-13 03:33:38 -08:00
|
|
|
self.path.items.len = self.path_indices.pop();
|
2021-05-29 00:51:17 -08:00
|
|
|
|
|
|
|
|
if (self.stat.dir) {
|
2021-07-13 03:33:38 -08:00
|
|
|
if (self.parents) |*p| {
|
2021-07-26 04:03:08 -08:00
|
|
|
if (p.items.len > 0) {
|
|
|
|
|
var d = p.pop();
|
|
|
|
|
d.final();
|
|
|
|
|
d.deinit();
|
|
|
|
|
}
|
2021-07-13 03:33:38 -08:00
|
|
|
}
|
2021-05-29 09:22:00 -08:00
|
|
|
if (self.wr) |w| w.writer().writeByte(']') catch |e| writeErr(e);
|
2021-05-29 00:51:17 -08:00
|
|
|
} else
|
|
|
|
|
self.stat.dir = true; // repeated popPath()s mean we're closing parent dirs.
|
2021-05-03 04:41:48 -08:00
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
fn pathZ(self: *Self) [:0]const u8 {
|
2021-05-29 03:18:21 -08:00
|
|
|
return arrayListBufZ(&self.path);
|
2021-05-09 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// 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 {
|
2021-07-26 04:03:08 -08:00
|
|
|
if (self.parents) |*p| p.items[p.items.len-1].dir.entry.setErr(p.items[p.items.len-1].dir);
|
2021-05-12 01:04:06 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Special = enum { err, other_fs, kernfs, excluded };
|
|
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
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(']');
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// Insert the current path as a special entry (i.e. a file/dir that is not counted)
|
2021-05-29 00:51:17 -08:00
|
|
|
// Ignores self.stat except for the 'dir' option.
|
2021-05-29 09:22:00 -08:00
|
|
|
fn addSpecial(self: *Self, t: Special) void {
|
2021-05-29 00:51:17 -08:00
|
|
|
std.debug.assert(self.items_seen > 0); // root item can't be a special
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (t == .err) {
|
|
|
|
|
if (self.last_error) |p| main.allocator.free(p);
|
2021-05-29 03:18:21 -08:00
|
|
|
self.last_error = main.allocator.dupeZ(u8, self.path.items) catch unreachable;
|
2021-05-12 01:04:06 -08:00
|
|
|
}
|
|
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
if (self.parents) |*p|
|
2021-07-26 04:03:08 -08:00
|
|
|
p.items[p.items.len-1].addSpecial(self.name, t)
|
2021-07-13 03:33:38 -08:00
|
|
|
else if (self.wr) |wr|
|
2021-05-29 09:22:00 -08:00
|
|
|
self.writeSpecial(wr.writer(), t) catch |e| writeErr(e);
|
|
|
|
|
|
2021-07-16 06:13:25 -08:00
|
|
|
self.stat.dir = false; // So that popPath() doesn't consider this as leaving a dir.
|
2021-05-29 00:51:17 -08:00
|
|
|
self.items_seen += 1;
|
2021-05-12 01:04:06 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
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}", .{ 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('}');
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// Insert current path as a counted file/dir/hardlink, with information from self.stat
|
2021-05-29 09:22:00 -08:00
|
|
|
fn addStat(self: *Self, dir_dev: u64) void {
|
2021-05-29 00:51:17 -08:00
|
|
|
if (self.parents) |*p| {
|
2021-07-26 04:03:08 -08:00
|
|
|
var e = if (p.items.len == 0) blk: {
|
2021-07-13 03:33:38 -08:00
|
|
|
// 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
|
2021-07-26 04:03:08 -08:00
|
|
|
p.items[p.items.len-1].addStat(self.name, &self.stat);
|
2021-07-13 03:33:38 -08:00
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
if (e.dir()) |d| // Enter the directory
|
|
|
|
|
p.append(ScanDir.init(d)) catch unreachable;
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
} else if (self.wr) |wr|
|
|
|
|
|
self.writeStat(wr.writer(), dir_dev) catch |e| writeErr(e);
|
2021-05-09 10:58:17 -08:00
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
self.items_seen += 1;
|
2021-05-09 10:58:17 -08:00
|
|
|
}
|
2021-05-23 07:18:49 -08:00
|
|
|
|
|
|
|
|
fn deinit(self: *Self) void {
|
|
|
|
|
if (self.last_error) |p| main.allocator.free(p);
|
2021-07-26 04:03:08 -08:00
|
|
|
if (self.parents) |*p| {
|
|
|
|
|
for (p.items) |*i| i.deinit();
|
|
|
|
|
p.deinit();
|
|
|
|
|
}
|
2021-05-29 00:51:17 -08:00
|
|
|
if (self.wr) |p| main.allocator.destroy(p);
|
2021-05-23 07:18:49 -08:00
|
|
|
self.path.deinit();
|
|
|
|
|
self.path_indices.deinit();
|
2021-07-13 03:33:38 -08:00
|
|
|
main.allocator.destroy(self);
|
2021-05-23 07:18:49 -08:00
|
|
|
}
|
2021-05-03 04:41:48 -08:00
|
|
|
};
|
|
|
|
|
|
2021-05-09 10:58:17 -08:00
|
|
|
// Context that is currently being used for scanning.
|
2021-07-13 03:33:38 -08:00
|
|
|
var active_context: *Context = undefined;
|
2021-05-09 10:58:17 -08:00
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
// Read and index entries of the given dir.
|
2021-05-29 09:22:00 -08:00
|
|
|
fn scanDir(ctx: *Context, dir: std.fs.Dir, dir_dev: u64) void {
|
2021-07-18 06:43:00 -08:00
|
|
|
var it = main.allocator.create(std.fs.Dir.Iterator) catch unreachable;
|
|
|
|
|
defer main.allocator.destroy(it);
|
|
|
|
|
it.* = dir.iterate();
|
2021-04-29 02:48:45 -08:00
|
|
|
while(true) {
|
|
|
|
|
const entry = it.next() catch {
|
2021-05-12 01:04:06 -08:00
|
|
|
ctx.setDirlistError();
|
2021-04-29 02:48:45 -08:00
|
|
|
return;
|
|
|
|
|
} orelse break;
|
|
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
ctx.stat.dir = false;
|
2021-05-29 03:18:21 -08:00
|
|
|
ctx.pushPath(entry.name);
|
2021-05-03 04:41:48 -08:00
|
|
|
defer ctx.popPath();
|
2021-05-29 03:18:21 -08:00
|
|
|
main.handleEvent(false, false);
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
// XXX: This algorithm is extremely slow, can be optimized with some clever pattern parsing.
|
|
|
|
|
const excluded = blk: {
|
|
|
|
|
for (main.config.exclude_patterns.items) |pat| {
|
2021-05-09 10:58:17 -08:00
|
|
|
var path = ctx.pathZ();
|
2021-05-03 04:41:48 -08:00
|
|
|
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) {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.excluded);
|
2021-05-03 04:41:48 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
ctx.stat = Stat.read(dir, ctx.name, false) catch {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.err);
|
2021-04-29 02:48:45 -08:00
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (main.config.same_fs and ctx.stat.dev != dir_dev) {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.other_fs);
|
2021-04-29 02:48:45 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (main.config.follow_symlinks and ctx.stat.symlink) {
|
2021-05-03 04:41:48 -08:00
|
|
|
if (Stat.read(dir, ctx.name, true)) |nstat| {
|
2021-04-30 09:15:29 -08:00
|
|
|
if (!nstat.dir) {
|
2021-05-12 01:04:06 -08:00
|
|
|
ctx.stat = nstat;
|
2021-04-30 09:15:29 -08:00
|
|
|
// Symlink targets may reside on different filesystems,
|
|
|
|
|
// this will break hardlink detection and counting so let's disable it.
|
2021-06-01 03:00:54 -08:00
|
|
|
if (ctx.stat.hlinkc and ctx.stat.dev != dir_dev)
|
|
|
|
|
ctx.stat.hlinkc = false;
|
2021-04-30 09:15:29 -08:00
|
|
|
}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var edir =
|
2021-05-12 01:04:06 -08:00
|
|
|
if (ctx.stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.err);
|
2021-04-30 09:15:29 -08:00
|
|
|
continue;
|
|
|
|
|
} else null;
|
|
|
|
|
defer if (edir != null) edir.?.close();
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (std.builtin.os.tag == .linux and main.config.exclude_kernfs and ctx.stat.dir and isKernfs(edir.?, ctx.stat.dev)) {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.kernfs);
|
2021-05-03 04:41:48 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (main.config.exclude_caches and ctx.stat.dir) {
|
2021-04-30 09:15:29 -08:00
|
|
|
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)) {
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addSpecial(.excluded);
|
2021-04-30 09:15:29 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
}
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
ctx.addStat(dir_dev);
|
|
|
|
|
if (ctx.stat.dir) scanDir(ctx, edir.?, ctx.stat.dev);
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
pub fn scanRoot(path: []const u8, out: ?std.fs.File) !void {
|
2021-07-26 04:03:08 -08:00
|
|
|
active_context = if (out) |f| Context.initFile(f) else Context.initMem(null);
|
2021-05-29 00:51:17 -08:00
|
|
|
|
|
|
|
|
const full_path = std.fs.realpathAlloc(main.allocator, path) catch null;
|
|
|
|
|
defer if (full_path) |p| main.allocator.free(p);
|
2021-07-13 03:33:38 -08:00
|
|
|
active_context.pushPath(full_path orelse path);
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
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();
|
|
|
|
|
}
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
pub fn setupRefresh(parent: *model.Dir) void {
|
|
|
|
|
active_context = Context.initMem(parent);
|
2021-07-13 03:33:38 -08:00
|
|
|
var full_path = std.ArrayList(u8).init(main.allocator);
|
|
|
|
|
defer full_path.deinit();
|
2021-07-26 04:03:08 -08:00
|
|
|
parent.fmtPath(true, &full_path);
|
2021-07-13 03:33:38 -08:00
|
|
|
active_context.pushPath(full_path.items);
|
|
|
|
|
active_context.stat.dir = true;
|
2021-07-28 00:29:15 -08:00
|
|
|
active_context.stat.dev = model.devices.list.items[parent.dev];
|
2021-07-13 03:33:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
};
|
2021-05-29 00:51:17 -08:00
|
|
|
defer dir.close();
|
2021-07-13 03:33:38 -08:00
|
|
|
scanDir(active_context, dir, active_context.stat.dev);
|
|
|
|
|
active_context.popPath();
|
|
|
|
|
active_context.final();
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
// 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 {
|
2021-07-13 03:33:38 -08:00
|
|
|
ctx: *Context,
|
2021-06-01 06:13:59 -08:00
|
|
|
|
|
|
|
|
rd: std.fs.File,
|
|
|
|
|
rdoff: usize = 0,
|
|
|
|
|
rdsize: usize = 0,
|
|
|
|
|
rdbuf: [8*1024]u8 = undefined,
|
|
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
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,
|
2021-05-12 01:04:06 -08:00
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
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 });
|
2021-05-12 01:04:06 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-29 00:51:17 -08:00
|
|
|
// Advance to the next byte, sets ch.
|
|
|
|
|
fn con(self: *Self) void {
|
2021-06-01 06:13:59 -08:00
|
|
|
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;
|
2021-05-29 00:51:17 -08:00
|
|
|
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 '\"'");
|
2021-07-17 00:10:24 -08:00
|
|
|
var n: usize = 0;
|
2021-05-29 00:51:17 -08:00
|
|
|
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"),
|
|
|
|
|
}
|
2021-05-12 01:04:06 -08:00
|
|
|
}
|
2021-05-29 00:51:17 -08:00
|
|
|
|
|
|
|
|
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")) {
|
2021-07-13 03:33:38 -08:00
|
|
|
self.ctx.stat.blocks = @intCast(model.Blocks, self.uint(u64)>>9);
|
2021-05-29 00:51:17 -08:00
|
|
|
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' => {
|
2021-06-01 03:00:54 -08:00
|
|
|
if (eq(u8, key, "hlnkc")) {
|
|
|
|
|
self.ctx.stat.hlinkc = self.boolean();
|
2021-05-29 00:51:17 -08:00
|
|
|
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")) {
|
2021-07-28 00:29:15 -08:00
|
|
|
self.ctx.stat.nlink = self.uint(u31);
|
2021-05-29 00:51:17 -08:00
|
|
|
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 '}'"),
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-29 03:18:21 -08:00
|
|
|
if (name) |n| self.ctx.pushPath(n)
|
2021-05-29 00:51:17 -08:00
|
|
|
else self.die("missing \"name\" field");
|
2021-05-29 09:22:00 -08:00
|
|
|
if (special) |s| self.ctx.addSpecial(s)
|
|
|
|
|
else self.ctx.addStat(dir_dev);
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn item(self: *Self, dev: u64) void {
|
|
|
|
|
self.ctx.stat = .{};
|
|
|
|
|
if (self.ch == '[') {
|
|
|
|
|
self.ctx.stat.dir = true;
|
|
|
|
|
self.con();
|
|
|
|
|
self.conws();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.iteminfo(dev);
|
|
|
|
|
|
|
|
|
|
self.conws();
|
|
|
|
|
if (self.ctx.stat.dir) {
|
|
|
|
|
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)
|
2021-05-29 03:18:21 -08:00
|
|
|
main.handleEvent(false, false);
|
2021-05-29 00:51:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-29 09:22:00 -08:00
|
|
|
pub fn importRoot(path: [:0]const u8, out: ?std.fs.File) void {
|
2021-05-29 00:51:17 -08:00
|
|
|
var fd = if (std.mem.eql(u8, "-", path)) std.io.getStdIn()
|
2021-05-29 09:22:00 -08:00
|
|
|
else std.fs.cwd().openFileZ(path, .{})
|
|
|
|
|
catch |e| ui.die("Error reading file: {s}.\n", .{ui.errorString(e)});
|
2021-05-29 00:51:17 -08:00
|
|
|
defer fd.close();
|
|
|
|
|
|
2021-07-26 04:03:08 -08:00
|
|
|
active_context = if (out) |f| Context.initFile(f) else Context.initMem(null);
|
2021-07-13 03:33:38 -08:00
|
|
|
var imp = Import{ .ctx = active_context, .rd = fd };
|
2021-05-29 00:51:17 -08:00
|
|
|
defer imp.ctx.deinit();
|
|
|
|
|
imp.root();
|
2021-05-29 09:22:00 -08:00
|
|
|
imp.ctx.final();
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
var animation_pos: u32 = 0;
|
2021-05-11 02:55:15 -08:00
|
|
|
var need_confirm_quit = false;
|
2021-05-09 10:58:17 -08:00
|
|
|
|
2021-07-13 03:33:38 -08:00
|
|
|
fn drawError(err: anyerror) void {
|
|
|
|
|
const width = saturateSub(ui.cols, 5);
|
|
|
|
|
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.?), saturateSub(width, 10)));
|
|
|
|
|
|
|
|
|
|
box.move(3, 2);
|
|
|
|
|
ui.addstr("Error: ");
|
|
|
|
|
ui.addstr(ui.shorten(ui.errorString(err), saturateSub(width, 6)));
|
|
|
|
|
|
|
|
|
|
box.move(5, saturateSub(width, 27));
|
|
|
|
|
ui.addstr("Press any key to continue");
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-29 03:18:21 -08:00
|
|
|
fn drawBox() void {
|
2021-05-09 10:58:17 -08:00
|
|
|
ui.init();
|
2021-07-13 03:33:38 -08:00
|
|
|
const ctx = active_context;
|
|
|
|
|
if (ctx.fatal_error) |err| return drawError(err);
|
2021-05-09 10:58:17 -08:00
|
|
|
const width = saturateSub(ui.cols, 5);
|
|
|
|
|
const box = ui.Box.create(10, width, "Scanning...");
|
|
|
|
|
box.move(2, 2);
|
|
|
|
|
ui.addstr("Total items: ");
|
|
|
|
|
ui.addnum(.default, ctx.items_seen);
|
|
|
|
|
|
2021-05-12 01:04:06 -08:00
|
|
|
if (width > 48 and ctx.parents != null) {
|
2021-05-09 10:58:17 -08:00
|
|
|
box.move(2, 30);
|
|
|
|
|
ui.addstr("size: ");
|
2021-07-28 00:29:15 -08:00
|
|
|
// TODO: Should display the size of the dir-to-be-refreshed on refreshing, not the root.
|
|
|
|
|
ui.addsize(.default, blocksToSize(saturateAdd(model.root.entry.blocks, model.inodes.total_blocks)));
|
2021-05-09 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box.move(3, 2);
|
|
|
|
|
ui.addstr("Current item: ");
|
2021-05-29 03:18:21 -08:00
|
|
|
ui.addstr(ui.shorten(ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
if (ctx.last_error) |path| {
|
|
|
|
|
box.move(5, 2);
|
|
|
|
|
ui.style(.bold);
|
|
|
|
|
ui.addstr("Warning: ");
|
|
|
|
|
ui.style(.default);
|
|
|
|
|
ui.addstr("error scanning ");
|
2021-05-29 03:18:21 -08:00
|
|
|
ui.addstr(ui.shorten(ui.toUtf8(path), saturateSub(width, 28)));
|
2021-05-09 10:58:17 -08:00
|
|
|
box.move(6, 3);
|
|
|
|
|
ui.addstr("some directory sizes may not be correct.");
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-11 02:55:15 -08:00
|
|
|
if (need_confirm_quit) {
|
|
|
|
|
box.move(8, saturateSub(width, 20));
|
|
|
|
|
ui.addstr("Press ");
|
|
|
|
|
ui.style(.key);
|
|
|
|
|
ui.addch('y');
|
|
|
|
|
ui.style(.default);
|
|
|
|
|
ui.addstr(" to confirm");
|
|
|
|
|
} else {
|
|
|
|
|
box.move(8, saturateSub(width, 18));
|
|
|
|
|
ui.addstr("Press ");
|
|
|
|
|
ui.style(.key);
|
|
|
|
|
ui.addch('q');
|
|
|
|
|
ui.style(.default);
|
|
|
|
|
ui.addstr(" to abort");
|
|
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
if (main.config.update_delay < std.time.ns_per_s and width > 40) {
|
|
|
|
|
const txt = "Scanning...";
|
|
|
|
|
animation_pos += 1;
|
|
|
|
|
if (animation_pos >= txt.len*2) animation_pos = 0;
|
|
|
|
|
if (animation_pos < txt.len) {
|
|
|
|
|
var i: u32 = 0;
|
|
|
|
|
box.move(8, 2);
|
|
|
|
|
while (i <= animation_pos) : (i += 1) ui.addch(txt[i]);
|
|
|
|
|
} else {
|
|
|
|
|
var i: u32 = txt.len-1;
|
|
|
|
|
while (i > animation_pos-txt.len) : (i -= 1) {
|
|
|
|
|
box.move(8, 2+i);
|
|
|
|
|
ui.addch(txt[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-29 03:18:21 -08:00
|
|
|
pub fn draw() void {
|
2021-07-28 01:13:03 -08:00
|
|
|
if (active_context.fatal_error != null and main.config.scan_ui != .full)
|
|
|
|
|
ui.die("Error reading {s}: {s}\n", .{ active_context.last_error.?, ui.errorString(active_context.fatal_error.?) });
|
2021-05-09 10:58:17 -08:00
|
|
|
switch (main.config.scan_ui) {
|
|
|
|
|
.none => {},
|
|
|
|
|
.line => {
|
|
|
|
|
var buf: [256]u8 = undefined;
|
|
|
|
|
var line: []const u8 = undefined;
|
2021-07-13 03:33:38 -08:00
|
|
|
if (active_context.parents == null) {
|
2021-05-09 10:58:17 -08:00
|
|
|
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
|
2021-07-13 03:33:38 -08:00
|
|
|
.{ ui.shorten(active_context.pathZ(), 63), active_context.items_seen }
|
2021-05-09 10:58:17 -08:00
|
|
|
) catch return;
|
|
|
|
|
} else {
|
|
|
|
|
const r = ui.FmtSize.fmt(blocksToSize(model.root.entry.blocks));
|
|
|
|
|
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <51} {d:>9} files / {s}{s}\x1b8",
|
2021-07-13 03:33:38 -08:00
|
|
|
.{ ui.shorten(active_context.pathZ(), 51), active_context.items_seen, r.num(), r.unit }
|
2021-05-09 10:58:17 -08:00
|
|
|
) catch return;
|
|
|
|
|
}
|
|
|
|
|
_ = std.io.getStdErr().write(line) catch {};
|
|
|
|
|
},
|
2021-05-29 03:18:21 -08:00
|
|
|
.full => drawBox(),
|
2021-05-09 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-29 03:18:21 -08:00
|
|
|
pub fn keyInput(ch: i32) void {
|
2021-07-13 03:33:38 -08:00
|
|
|
if (active_context.fatal_error != null) {
|
|
|
|
|
if (main.state == .scan) ui.quit()
|
|
|
|
|
else main.state = .browse;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-11 02:55:15 -08:00
|
|
|
if (need_confirm_quit) {
|
|
|
|
|
switch (ch) {
|
|
|
|
|
'y', 'Y' => if (need_confirm_quit) ui.quit(),
|
|
|
|
|
else => need_confirm_quit = false,
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
switch (ch) {
|
2021-05-11 02:55:15 -08:00
|
|
|
'q' => if (main.config.confirm_quit) { need_confirm_quit = true; } else ui.quit(),
|
|
|
|
|
else => need_confirm_quit = false,
|
2021-05-09 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
}
|