2021-04-29 02:48:45 -08:00
|
|
|
const std = @import("std");
|
|
|
|
|
const main = @import("main.zig");
|
|
|
|
|
const model = @import("model.zig");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
ext: model.Ext,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cast any integer type to the unsigned target type, wrapping/truncating as necessary.
|
|
|
|
|
fn castWrap(comptime T: type, x: anytype) T {
|
|
|
|
|
return @intCast(T, x); // TODO
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wrap(comptime T: type, comptime field: anytype, x: anytype) std.meta.fieldInfo(T, field).field_type {
|
|
|
|
|
return castWrap(std.meta.fieldInfo(T, field).field_type, x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn readStat(parent: std.fs.Dir, name: [:0]const u8) !Stat {
|
|
|
|
|
const stat = try std.os.fstatatZ(parent.fd, name, 0);
|
|
|
|
|
return Stat{
|
|
|
|
|
.blocks = clamp(Stat, .blocks, stat.blocks),
|
|
|
|
|
.size = clamp(Stat, .size, stat.size),
|
|
|
|
|
.dev = wrap(Stat, .dev, stat.dev),
|
|
|
|
|
.ino = wrap(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),
|
|
|
|
|
.ext = .{
|
|
|
|
|
.mtime = clamp(model.Ext, .mtime, stat.mtime().tv_sec),
|
|
|
|
|
.uid = wrap(model.Ext, .uid, stat.uid),
|
|
|
|
|
.gid = wrap(model.Ext, .gid, stat.gid),
|
|
|
|
|
.mode = clamp(model.Ext, .mode, stat.mode & 0xffff),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read and index entries of the dir identified by parent/parents.top().
|
|
|
|
|
// (TODO: shouldn't error on OOM but instead call a function that waits or something)
|
|
|
|
|
fn scanDir(parents: *model.Parents, parent: std.fs.Dir) std.mem.Allocator.Error!void {
|
|
|
|
|
var dir = parent.openDirZ(parents.top().entry.name(), .{ .access_sub_paths = true, .iterate = true, .no_follow = true }) catch {
|
|
|
|
|
parents.top().entry.set_err(parents);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
defer dir.close();
|
|
|
|
|
|
|
|
|
|
var it = dir.iterate();
|
|
|
|
|
while(true) {
|
|
|
|
|
const entry = it.next() catch {
|
|
|
|
|
parents.top().entry.set_err(parents);
|
|
|
|
|
return;
|
|
|
|
|
} orelse break;
|
|
|
|
|
|
|
|
|
|
// TODO: Check for exclude patterns
|
|
|
|
|
|
|
|
|
|
// XXX: Surely the name already has a trailing \0 in the buffer received by the OS?
|
|
|
|
|
const name_z = std.os.toPosixPath(entry.name) catch undefined;
|
|
|
|
|
const stat = readStat(dir, &name_z) catch {
|
|
|
|
|
var e = try model.Entry.create(.file, false, entry.name);
|
|
|
|
|
e.insert(parents) catch unreachable;
|
|
|
|
|
e.set_err(parents);
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (main.config.same_fs and stat.dev != model.getDev(parents.top().dev)) {
|
|
|
|
|
var e = try model.Entry.create(.file, false, entry.name);
|
|
|
|
|
e.file().?.other_fs = true;
|
|
|
|
|
e.insert(parents) catch unreachable;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO Check for kernfs
|
|
|
|
|
// TODO Follow symlink if that option is enabled
|
|
|
|
|
// TODO Check for CACHEDIR.TAG if that option is enabled and this is a dir
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
if (e.dir()) |d| {
|
|
|
|
|
d.dev = try model.getDevId(stat.dev);
|
|
|
|
|
// The dir entry itself also counts.
|
|
|
|
|
d.total_blocks = stat.blocks;
|
|
|
|
|
d.total_size = stat.size;
|
|
|
|
|
d.total_items = 1;
|
|
|
|
|
}
|
|
|
|
|
if (e.ext()) |ext| ext.* = stat.ext;
|
|
|
|
|
if (e.link()) |l| {
|
|
|
|
|
l.ino = stat.ino;
|
|
|
|
|
l.nlink = stat.nlink;
|
|
|
|
|
}
|
|
|
|
|
try e.insert(parents);
|
|
|
|
|
|
|
|
|
|
if (e.dir()) |d| {
|
|
|
|
|
try parents.push(d);
|
|
|
|
|
try scanDir(parents, dir);
|
|
|
|
|
parents.pop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-29 08:59:25 -08:00
|
|
|
pub fn scanRoot(path: []const u8) !void {
|
|
|
|
|
// XXX: Both realpathAlloc() and toPosixPath are limited to PATH_MAX.
|
|
|
|
|
// Oh well, I suppose we can accept that as limitation for the top-level dir we're scanning.
|
|
|
|
|
const full_path = try std.os.toPosixPath(try std.fs.realpathAlloc(main.allocator, path));
|
|
|
|
|
|
|
|
|
|
const stat = try readStat(std.fs.cwd(), &full_path);
|
2021-04-29 02:48:45 -08:00
|
|
|
if (!stat.dir) return error.NotADirectory;
|
2021-04-29 08:59:25 -08:00
|
|
|
model.root = (try model.Entry.create(.dir, false, &full_path)).dir().?;
|
2021-04-29 02:48:45 -08:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
var parents = model.Parents{};
|
|
|
|
|
try scanDir(&parents, std.fs.cwd());
|
|
|
|
|
}
|