diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 06:17:47 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 07:16:43 -0300 |
| commit | df10c37122cc3e6565d13aeefb9466dc6c0dc780 (patch) | |
| tree | 5d8492f59c5488f1c4c5abda514b79e5d34945ea /src | |
| parent | 94f0c9359dc1b8ccd73bf58812bec9e78fb52073 (diff) | |
| download | pardes-df10c37122cc3e6565d13aeefb9466dc6c0dc780.tar.gz pardes-df10c37122cc3e6565d13aeefb9466dc6c0dc780.zip | |
Grep and Find pass over a directory they may not open and keep every other hit, saying in one line how many they skipped
One `chmod 000` directory anywhere under a pane made every Grep say
`pattern not found` and every Find `access denied`: the walk's `try
w.enter`/`try w.next` ended it at the first refusal. Each is now counted
and passed over, and the +Search ends `N directories skipped:
permission denied` under the hits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/fs.zig | 57 | ||||
| -rw-r--r-- | src/look.zig | 12 |
2 files changed, 60 insertions, 9 deletions
@@ -1079,7 +1079,10 @@ const find_skip = [_][]const u8{ ".venv", "__pycache__", ".zig-cache", "zig-out", }; -pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: []u8) !usize { +/// A directory the walk may not open or read is passed over and counted in +/// `skipped`, its siblings' hits kept: one locked directory is not every +/// search's failure. +pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: []u8, skipped: *usize) !usize { var hits: [find_max_hits][]const u8 = undefined; var hits_len: usize = 0; if (platform_has_fs) { @@ -1093,14 +1096,19 @@ pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: []u var steps: usize = 0; walk: while (steps < find_max_steps and hits_len < hits.len) { steps += 1; // an unreadable dir burns a step too, so it cannot spin - const e = (try w.next(io)) orelse break; + const e = (w.next(io) catch { + skipped.* += 1; + continue; + }) orelse break; if (std.ascii.indexOfIgnoreCase(e.basename, pat) != null) { hits[hits_len] = try arena.dupe(u8, e.path); hits_len += 1; } if (e.kind != .directory or e.depth() >= find_max_depth) continue; for (find_skip) |s| if (std.mem.eql(u8, e.basename, s)) continue :walk; - try w.enter(io, e); + w.enter(io, e) catch { + skipped.* += 1; + }; } } else { for (sources) |s| { @@ -1152,7 +1160,7 @@ fn grepText(path: []const u8, text: []const u8, pat: []const u8, out: []u8, budg return result; } -pub fn grep(arena: std.mem.Allocator, gpa: std.mem.Allocator, dir: []const u8, base: []const u8, pat: []const u8, out: []u8) !usize { +pub fn grep(arena: std.mem.Allocator, gpa: std.mem.Allocator, dir: []const u8, base: []const u8, pat: []const u8, out: []u8, skipped: *usize) !usize { var hits: usize = 0; var written: usize = 0; if (!platform_has_fs) { @@ -1180,11 +1188,16 @@ pub fn grep(arena: std.mem.Allocator, gpa: std.mem.Allocator, dir: []const u8, b var steps: usize = 0; walk: while (steps < find_max_steps and files_len < files.len) { steps += 1; - const e = (try w.next(io)) orelse break; + const e = (w.next(io) catch { + skipped.* += 1; + continue; + }) orelse break; if (e.kind == .directory) { if (e.depth() >= find_max_depth) continue; for (find_skip) |s| if (std.mem.eql(u8, e.basename, s)) continue :walk; - try w.enter(io, e); + w.enter(io, e) catch { + skipped.* += 1; + }; continue; } if (e.kind != .file) continue; @@ -1253,13 +1266,43 @@ test "grep skips a file it cannot read instead of abandoning the search" { defer arena.deinit(); const out = try gpa.alloc(u8, 64 * 1024); defer gpa.free(out); - const n = try grep(arena.allocator(), gpa, dir, dir, "needle", out); + var skipped: usize = 0; + const n = try grep(arena.allocator(), gpa, dir, dir, "needle", out, &skipped); _ = libc.chmod(locked, 0o644); // so `tmp.cleanup` can remove it try std.testing.expect(std.mem.indexOf(u8, out[0..n], "b-open.txt") != null); try std.testing.expect(std.mem.indexOf(u8, out[0..n], "a-locked.txt") == null); } +test "a directory the walk may not open is skipped and counted: Grep and Find keep every other hit" { + if (!platform_has_fs) return; + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var base_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir = base_buf[0..try tmp.dir.realPath(std.testing.io, &base_buf)]; + try tmp.dir.createDirPath(std.testing.io, "locked"); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "locked/needle-in.txt", .data = "needle\n" }); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "needle-out.txt", .data = "needle\n" }); + var locked_buf: [std.fs.max_path_bytes]u8 = undefined; + const locked = try std.fmt.bufPrintSentinel(&locked_buf, "{s}/locked", .{dir}, 0); + if (libc.chmod(locked, 0) != 0) return; + defer _ = libc.chmod(locked, 0o755); // so `tmp.cleanup` can remove it + if (isDir(locked)) return error.SkipZigTest; // root reads it anyway + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + const out = try gpa.alloc(u8, 64 * 1024); + defer gpa.free(out); + var skipped: usize = 0; + var n = try grep(arena.allocator(), gpa, dir, dir, "needle", out, &skipped); + try std.testing.expect(std.mem.indexOf(u8, out[0..n], "needle-out.txt:1:") != null); + try std.testing.expectEqual(@as(usize, 1), skipped); + skipped = 0; + n = try find(arena.allocator(), dir, "needle", out, &skipped); + try std.testing.expect(std.mem.indexOf(u8, out[0..n], "needle-out.txt") != null); + try std.testing.expectEqual(@as(usize, 1), skipped); +} + fn isDir(path: [*:0]const u8) bool { const fd = libc.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }); if (fd < 0) return false; diff --git a/src/look.zig b/src/look.zig index f208c1fb..21148b37 100644 --- a/src/look.zig +++ b/src/look.zig @@ -593,6 +593,7 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start var out_len: usize = 0; var nrows: usize = 0; var anchor: ?usize = null; + var skipped: usize = 0; if (kind == .grep) { var roots: [MAX_PANES][]const u8 = undefined; var nroots: usize = 0; @@ -625,13 +626,13 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start // not made yet, a peer's) has nothing to grep: passed over, not the // search's failure. for (roots[0..nroots]) |r| - out_len += filesystem.grep(arena, p.gpa, r, dir, pat, out[out_len..]) catch |err| switch (err) { + out_len += filesystem.grep(arena, p.gpa, r, dir, pat, out[out_len..], &skipped) catch |err| switch (err) { error.FileNotFound, error.NotDir, error.AccessDenied => continue, else => return err, }; nrows = std.mem.count(u8, out[0..out_len], "\n"); } else if (kind == .find) { - out_len = try filesystem.find(arena, dir, pat, out); + out_len = try filesystem.find(arena, dir, pat, out, &skipped); nrows = std.mem.count(u8, out[0..out_len], "\n"); } else if (pane.hasPdf()) { if (comptime pdf_enabled) { @@ -672,6 +673,13 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start nrows += 1; } } + // What the walk could not open is said in one line under the hits, so + // a locked directory reads as passed over, not as nothing there. + if (skipped > 0) { + const note = std.fmt.bufPrint(out[out_len..], "{d} director{s} skipped: permission denied\n", .{ skipped, if (skipped == 1) "y" else "ies" }) catch ""; + out_len += note.len; + nrows += @intFromBool(note.len > 0); + } // A Find or Grep that finds nothing empties its directory's +Search, if // there is one, rather than leave the last search's rows looking like // this one's. |
