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 {
|
|
|
|
|
blocks: u61,
|
|
|
|
|
size: u64,
|
|
|
|
|
dev: u64,
|
|
|
|
|
ino: u64,
|
|
|
|
|
nlink: u32,
|
|
|
|
|
dir: bool,
|
|
|
|
|
reg: bool,
|
2021-04-30 09:15:29 -08:00
|
|
|
symlink: bool,
|
2021-04-29 02:48:45 -08:00
|
|
|
ext: model.Ext,
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
// Cast any integer type to the target type, clamping the value to the supported maximum if necessary.
|
|
|
|
|
fn castClamp(comptime T: type, x: anytype) T {
|
|
|
|
|
// (adapted from std.math.cast)
|
|
|
|
|
if (std.math.maxInt(@TypeOf(x)) > std.math.maxInt(T) and x > std.math.maxInt(T)) {
|
|
|
|
|
return std.math.maxInt(T);
|
|
|
|
|
} else if (std.math.minInt(@TypeOf(x)) < std.math.minInt(T) and x < std.math.minInt(T)) {
|
|
|
|
|
return std.math.minInt(T);
|
|
|
|
|
} else {
|
|
|
|
|
return @intCast(T, x);
|
|
|
|
|
}
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
// Cast any integer type to the target type, truncating if necessary.
|
|
|
|
|
fn castTruncate(comptime T: type, x: anytype) T {
|
|
|
|
|
const Ti = @typeInfo(T).Int;
|
|
|
|
|
const Xi = @typeInfo(@TypeOf(x)).Int;
|
|
|
|
|
const nx = if (Xi.signedness != Ti.signedness) @bitCast(std.meta.Int(Ti.signedness, Xi.bits), x) else x;
|
|
|
|
|
return if (Xi.bits > Ti.bits) @truncate(T, nx) else nx;
|
|
|
|
|
}
|
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),
|
|
|
|
|
.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-03 04:41:48 -08:00
|
|
|
const Context = struct {
|
|
|
|
|
parents: model.Parents = .{},
|
|
|
|
|
path: std.ArrayList(u8) = std.ArrayList(u8).init(main.allocator),
|
|
|
|
|
path_indices: std.ArrayList(usize) = std.ArrayList(usize).init(main.allocator),
|
2021-05-09 10:58:17 -08:00
|
|
|
items_seen: u32 = 1,
|
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-05-03 04:41:48 -08:00
|
|
|
const Self = @This();
|
|
|
|
|
|
|
|
|
|
fn pushPath(self: *Self, name: []const u8) !void {
|
|
|
|
|
try self.path_indices.append(self.path.items.len);
|
|
|
|
|
if (self.path.items.len > 1) try self.path.append('/');
|
|
|
|
|
const start = self.path.items.len;
|
|
|
|
|
try self.path.appendSlice(name);
|
|
|
|
|
|
|
|
|
|
try self.path.append(0);
|
|
|
|
|
self.name = self.path.items[start..self.path.items.len-1:0];
|
|
|
|
|
self.path.items.len -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn popPath(self: *Self) void {
|
|
|
|
|
self.path.items.len = self.path_indices.items[self.path_indices.items.len-1];
|
|
|
|
|
self.path_indices.items.len -= 1;
|
|
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
fn pathZ(self: *Self) [:0]const u8 {
|
|
|
|
|
self.path.append(0) catch unreachable;
|
|
|
|
|
defer self.path.items.len -= 1;
|
|
|
|
|
return self.path.items[0..self.path.items.len-1:0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert the current path as an error entry
|
|
|
|
|
fn setError(self: *Self) !void {
|
|
|
|
|
var e = try model.Entry.create(.file, false, self.name);
|
|
|
|
|
e.insert(&self.parents) catch unreachable;
|
|
|
|
|
e.set_err(&self.parents);
|
|
|
|
|
|
|
|
|
|
if (self.last_error) |p| main.allocator.free(p);
|
|
|
|
|
self.last_error = try main.allocator.dupeZ(u8, self.path.items);
|
|
|
|
|
}
|
2021-05-03 04:41:48 -08:00
|
|
|
};
|
|
|
|
|
|
2021-05-09 10:58:17 -08:00
|
|
|
// Context that is currently being used for scanning.
|
|
|
|
|
var active_context: ?*Context = null;
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
// Read and index entries of the given dir. The entry for the directory is already assumed to be in 'ctx.parents'.
|
2021-04-29 02:48:45 -08:00
|
|
|
// (TODO: shouldn't error on OOM but instead call a function that waits or something)
|
2021-05-03 04:41:48 -08:00
|
|
|
fn scanDir(ctx: *Context, dir: std.fs.Dir) std.mem.Allocator.Error!void {
|
2021-04-29 02:48:45 -08:00
|
|
|
var it = dir.iterate();
|
|
|
|
|
while(true) {
|
|
|
|
|
const entry = it.next() catch {
|
2021-05-03 04:41:48 -08:00
|
|
|
ctx.parents.top().entry.set_err(&ctx.parents);
|
2021-04-29 02:48:45 -08:00
|
|
|
return;
|
|
|
|
|
} orelse break;
|
2021-05-09 10:58:17 -08:00
|
|
|
ctx.items_seen += 1;
|
2021-04-29 02:48:45 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
try ctx.pushPath(entry.name);
|
2021-05-09 10:58:17 -08:00
|
|
|
try main.handleEvent(false, false);
|
2021-05-03 04:41:48 -08:00
|
|
|
defer ctx.popPath();
|
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-04-29 02:48:45 -08:00
|
|
|
var e = try model.Entry.create(.file, false, entry.name);
|
2021-05-03 04:41:48 -08:00
|
|
|
e.file().?.excluded = true;
|
|
|
|
|
e.insert(&ctx.parents) catch unreachable;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var stat = Stat.read(dir, ctx.name, false) catch {
|
2021-05-09 10:58:17 -08:00
|
|
|
try ctx.setError();
|
2021-04-29 02:48:45 -08:00
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
if (main.config.same_fs and stat.dev != model.getDev(ctx.parents.top().dev)) {
|
2021-05-09 10:58:17 -08:00
|
|
|
try ctx.setError();
|
2021-04-29 02:48:45 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-30 09:15:29 -08:00
|
|
|
if (main.config.follow_symlinks and 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) {
|
|
|
|
|
stat = nstat;
|
|
|
|
|
// Symlink targets may reside on different filesystems,
|
|
|
|
|
// this will break hardlink detection and counting so let's disable it.
|
2021-05-03 04:41:48 -08:00
|
|
|
if (stat.nlink > 1 and stat.dev != model.getDev(ctx.parents.top().dev))
|
2021-04-30 09:15:29 -08:00
|
|
|
stat.nlink = 1;
|
|
|
|
|
}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var edir =
|
2021-05-03 04:41:48 -08:00
|
|
|
if (stat.dir) dir.openDirZ(ctx.name, .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
|
2021-05-09 10:58:17 -08:00
|
|
|
try ctx.setError();
|
2021-04-30 09:15:29 -08:00
|
|
|
continue;
|
|
|
|
|
} else null;
|
|
|
|
|
defer if (edir != null) edir.?.close();
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
if (std.builtin.os.tag == .linux and main.config.exclude_kernfs and stat.dir and isKernfs(edir.?, stat.dev)) {
|
|
|
|
|
var e = try model.Entry.create(.file, false, entry.name);
|
|
|
|
|
e.file().?.kernfs = true;
|
|
|
|
|
e.insert(&ctx.parents) catch unreachable;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-30 09:15:29 -08:00
|
|
|
if (main.config.exclude_caches and stat.dir) {
|
|
|
|
|
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)) {
|
|
|
|
|
var e = try model.Entry.create(.file, false, entry.name);
|
|
|
|
|
e.file().?.excluded = true;
|
2021-05-03 04:41:48 -08:00
|
|
|
e.insert(&ctx.parents) catch unreachable;
|
2021-04-30 09:15:29 -08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
} else |_| {}
|
|
|
|
|
}
|
2021-04-29 02:48:45 -08:00
|
|
|
|
|
|
|
|
const etype = if (stat.dir) model.EType.dir else if (stat.nlink > 1) model.EType.link else model.EType.file;
|
|
|
|
|
var e = try model.Entry.create(etype, main.config.extended, entry.name);
|
|
|
|
|
e.blocks = stat.blocks;
|
|
|
|
|
e.size = stat.size;
|
2021-05-06 09:15:47 -08:00
|
|
|
if (e.dir()) |d| d.dev = try model.getDevId(stat.dev);
|
2021-04-30 09:15:29 -08:00
|
|
|
if (e.file()) |f| f.notreg = !stat.dir and !stat.reg;
|
2021-04-29 02:48:45 -08:00
|
|
|
if (e.link()) |l| {
|
|
|
|
|
l.ino = stat.ino;
|
|
|
|
|
l.nlink = stat.nlink;
|
|
|
|
|
}
|
2021-04-30 09:15:29 -08:00
|
|
|
if (e.ext()) |ext| ext.* = stat.ext;
|
2021-05-03 04:41:48 -08:00
|
|
|
try e.insert(&ctx.parents);
|
2021-04-29 02:48:45 -08:00
|
|
|
|
|
|
|
|
if (e.dir()) |d| {
|
2021-05-03 04:41:48 -08:00
|
|
|
try ctx.parents.push(d);
|
|
|
|
|
try scanDir(ctx, edir.?);
|
|
|
|
|
ctx.parents.pop();
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-29 08:59:25 -08:00
|
|
|
pub fn scanRoot(path: []const u8) !void {
|
2021-05-01 00:39:57 -08:00
|
|
|
const full_path = std.fs.realpathAlloc(main.allocator, path) catch path;
|
|
|
|
|
model.root = (try model.Entry.create(.dir, false, full_path)).dir().?;
|
2021-04-29 08:59:25 -08:00
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
const stat = try Stat.read(std.fs.cwd(), model.root.entry.name(), true);
|
2021-04-29 02:48:45 -08:00
|
|
|
if (!stat.dir) return error.NotADirectory;
|
|
|
|
|
model.root.entry.blocks = stat.blocks;
|
|
|
|
|
model.root.entry.size = stat.size;
|
|
|
|
|
model.root.dev = try model.getDevId(stat.dev);
|
|
|
|
|
if (model.root.entry.ext()) |ext| ext.* = stat.ext;
|
|
|
|
|
|
2021-05-03 04:41:48 -08:00
|
|
|
var ctx = Context{};
|
|
|
|
|
try ctx.pushPath(full_path);
|
2021-05-01 00:39:57 -08:00
|
|
|
const dir = try std.fs.cwd().openDirZ(model.root.entry.name(), .{ .access_sub_paths = true, .iterate = true });
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
active_context = &ctx;
|
|
|
|
|
defer active_context = null;
|
2021-05-03 04:41:48 -08:00
|
|
|
try scanDir(&ctx, dir);
|
2021-04-29 02:48:45 -08:00
|
|
|
}
|
2021-05-09 10:58:17 -08:00
|
|
|
|
|
|
|
|
var animation_pos: u32 = 0;
|
|
|
|
|
|
|
|
|
|
fn drawBox() !void {
|
|
|
|
|
ui.init();
|
|
|
|
|
const ctx = active_context.?;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
if (width > 48 and true) { // TODO: When not exporting to file
|
|
|
|
|
box.move(2, 30);
|
|
|
|
|
ui.addstr("size: ");
|
|
|
|
|
ui.addsize(.default, blocksToSize(model.root.entry.blocks));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box.move(3, 2);
|
|
|
|
|
ui.addstr("Current item: ");
|
|
|
|
|
ui.addstr(try ui.shorten(try ui.toUtf8(ctx.pathZ()), saturateSub(width, 18)));
|
|
|
|
|
|
|
|
|
|
if (ctx.last_error) |path| {
|
|
|
|
|
box.move(5, 2);
|
|
|
|
|
ui.style(.bold);
|
|
|
|
|
ui.addstr("Warning: ");
|
|
|
|
|
ui.style(.default);
|
|
|
|
|
ui.addstr("error scanning ");
|
|
|
|
|
ui.addstr(try ui.shorten(try ui.toUtf8(path), saturateSub(width, 28)));
|
|
|
|
|
box.move(6, 3);
|
|
|
|
|
ui.addstr("some directory sizes may not be correct.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box.move(8, saturateSub(width, 18));
|
|
|
|
|
ui.addstr("Press ");
|
|
|
|
|
ui.style(.key);
|
|
|
|
|
ui.addch('q');
|
|
|
|
|
ui.style(.default);
|
|
|
|
|
ui.addstr(" to abort");
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn draw() !void {
|
|
|
|
|
switch (main.config.scan_ui) {
|
|
|
|
|
.none => {},
|
|
|
|
|
.line => {
|
|
|
|
|
var buf: [256]u8 = undefined;
|
|
|
|
|
var line: []const u8 = undefined;
|
|
|
|
|
if (false) { // TODO: When exporting to file; no total size known
|
|
|
|
|
line = std.fmt.bufPrint(&buf, "\x1b7\x1b[J{s: <63} {d:>9} files\x1b8",
|
|
|
|
|
.{ ui.shorten(active_context.?.pathZ(), 63), active_context.?.items_seen }
|
|
|
|
|
) 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",
|
|
|
|
|
.{ ui.shorten(active_context.?.pathZ(), 51), active_context.?.items_seen, r.num(), r.unit }
|
|
|
|
|
) catch return;
|
|
|
|
|
}
|
|
|
|
|
_ = std.io.getStdErr().write(line) catch {};
|
|
|
|
|
},
|
|
|
|
|
.full => try drawBox(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn key(ch: i32) !void {
|
|
|
|
|
switch (ch) {
|
|
|
|
|
'q' => ui.quit(), // TODO: Confirm quit
|
|
|
|
|
else => {},
|
|
|
|
|
}
|
|
|
|
|
}
|