summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig62
1 files changed, 62 insertions, 0 deletions
diff --git a/src/look.zig b/src/look.zig
index f34fa367..61970de1 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -170,6 +170,68 @@ const platform_has_fs = switch (pardes.platform) {
.web => false,
};
+// Find's safety rails. The core is SYNCHRONOUS — a Find at `/` runs inside the
+// keystroke that asked for it — so the walk must end whatever it is pointed at.
+// Three caps, because each alone leaks: hits bound the results buffer, depth
+// bounds a deep tree, and steps bound a wide shallow one (a pattern that never
+// matches would otherwise walk the whole disk without ever filling `hits`).
+const find_max_hits = 512;
+const find_max_depth = 16;
+const find_max_steps = 100_000;
+
+/// `fd`, in-core: every path under `dir` whose NAME contains `pat` (plain
+/// case-insensitive substring — fd's default is a regex and pardes has no
+/// regex engine to spend on one), one ABSOLUTE path per line into `out`.
+/// Absolute because each line is an ordinary look target that outlives the
+/// walk: a terminal's cwd can move out from under its own results buffer.
+/// Only real directories are entered, so a symlink can never close a cycle,
+/// and `.git` is skipped whole (fd's default too).
+pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: *std.ArrayList(u8)) void {
+ var hits: std.ArrayList([]const u8) = .empty;
+ if (platform_has_fs) {
+ // Zig 0.16 moved the filesystem behind std.Io; the blocking
+ // single-threaded implementation (the one std.debug itself holds) IS
+ // the synchronous walk a sans-IO core wants — no pool, no cancelation.
+ const io = std.Io.Threaded.global_single_threaded.io();
+ var root = std.Io.Dir.cwd().openDir(io, dir, .{ .iterate = true }) catch return;
+ defer root.close(io);
+ // walkSelectively, not walk: descending is opt-in, which is the only
+ // way to express the depth cap and the .git skip at all.
+ var w = root.walkSelectively(arena) catch return;
+ defer w.deinit();
+ var steps: usize = 0;
+ while (steps < find_max_steps and hits.items.len < find_max_hits) {
+ steps += 1; // an unreadable dir burns a step too, so it cannot spin
+ const e = (w.next(io) catch continue) orelse break;
+ if (std.ascii.indexOfIgnoreCase(e.basename, pat) != null)
+ hits.append(arena, std.fmt.allocPrint(arena, "{s}/{s}", .{
+ std.mem.trimEnd(u8, dir, "/"), e.path,
+ }) catch break) catch break;
+ if (e.kind == .directory and e.depth() < find_max_depth and !std.mem.eql(u8, e.basename, ".git"))
+ w.enter(io, e) catch {};
+ }
+ } else {
+ // web: the build-generated source archive IS the filesystem, and it is
+ // already a flat list of paths — the whole walk is the match.
+ for (embedded_sources.all) |s| {
+ if (hits.items.len >= find_max_hits) break;
+ if (std.ascii.indexOfIgnoreCase(std.fs.path.basename(s.path), pat) != null)
+ hits.append(arena, s.path) catch break;
+ }
+ }
+ // readdir order is undefined; sort so the same tree gives the same buffer
+ // twice running and n/N walks it in a sane order.
+ std.mem.sort([]const u8, hits.items, {}, struct {
+ fn lt(_: void, a: []const u8, b: []const u8) bool {
+ return std.mem.lessThan(u8, a, b);
+ }
+ }.lt);
+ for (hits.items) |h| {
+ out.appendSlice(arena, h) catch return;
+ out.append(arena, '\n') catch return;
+ }
+}
+
/// true if `path` exists and is a directory (open(O_DIRECTORY), no stat needed)
fn isDir(path: [*:0]const u8) bool {
const fd = libc.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true });