summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:45:38 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit157df8cb547ba4cf900b1f9eb75df15384985708 (patch)
tree248a415fe9431989e9468ef001a1dd364e249afe /src
parent2c03f710535a0b0aa143cd6c1fad04a45857a777 (diff)
downloadpardes-157df8cb547ba4cf900b1f9eb75df15384985708.tar.gz
pardes-157df8cb547ba4cf900b1f9eb75df15384985708.zip
A served-tree pane spoils no Grep, and `look .` from a gone directory is ENOENT
A pane named `/virtual` gave Grep the root `/`, which swallowed every other pane's directory and ran out of walk before any file: every Grep missed. Grep passes over panes of the served tree. And `.` from a pane whose directory was gone resolved to the served tree's root (/virtual/), or through the jump list to another pane's directory: a dot path is the looking pane's own, and gone it fails, `look: <dir>: no such directory`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/fs.zig12
-rw-r--r--src/look.zig22
-rw-r--r--src/ninep/ctl.zig25
3 files changed, 56 insertions, 3 deletions
diff --git a/src/fs.zig b/src/fs.zig
index 352aa0c4..de02e8e8 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -677,7 +677,9 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409
if (std.mem.startsWith(u8, joined, "/virtual/")) return resolveVirtual(p, joined[8..], out);
if (resolveOs(joined, out)) |found| return found;
if (resolveVirtual(p, joined, out)) |found| return found;
- if (resolveVirtual(p, word, out)) |found| return found;
+ // `.`, `..` and `./x` name the pane's directory, never the served tree:
+ // with that directory gone they name nothing (`.` would be /virtual/).
+ if (!dotRelative(word)) if (resolveVirtual(p, word, out)) |found| return found;
if (resolveEmbedded(word, cwd, out)) |source| {
const path = std.fmt.bufPrint(out, "/virtual/{s}", .{source.path}) catch return null;
return .{ .path = path };
@@ -685,6 +687,14 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409
return null;
}
+/// Whether `word` is spelled relative to the directory it is looked from:
+/// `.`, `..`, `./x`, `../x`.
+pub fn dotRelative(word: []const u8) bool {
+ const w = std.mem.trimEnd(u8, word, "/");
+ return std.mem.eql(u8, w, ".") or std.mem.eql(u8, w, "..") or
+ std.mem.startsWith(u8, word, "./") or std.mem.startsWith(u8, word, "../");
+}
+
/// Where a path really is on the host, and whether it is a directory. The
/// path may lie in a mount this editor serves, so the turn is given up for
/// the syscalls: another thread answers them.
diff --git a/src/look.zig b/src/look.zig
index fa86aee4..9a81a5c9 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -593,7 +593,11 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start
var nroots: usize = 0;
for (p.panes) |slot| {
const pp = slot orelse continue;
- const d = Pardes.paneDir(pp);
+ // A pane of the served tree (/virtual/, /n/<peer>/) has no
+ // directory on this host to walk, and one that named `/` would
+ // swallow every other root: passed over.
+ const d = filesystem.localPath(Pardes.paneDir(pp)) orelse continue;
+ if (filesystem.isVirtual(if (pp.file) |f| f.path else d)) continue;
var covered = false;
var k: usize = 0;
while (k < nroots) {
@@ -1107,7 +1111,8 @@ fn resolveLookTarget(p: *Pardes, id: usize, txt: []const u8, realbuf: *[4096]u8)
if (openPaneTarget(p, j, pl.at)) |target| return target;
}
found = resolve(p, txt, dir, realbuf);
- if (found != .none or pl.path.len == 0 or pl.path[0] == '/') break;
+ // `.`, `..`, `./x` are this pane's directory's, not another's.
+ if (found != .none or pl.path.len == 0 or pl.path[0] == '/' or filesystem.dotRelative(pl.path)) break;
}
return found;
}
@@ -1407,6 +1412,13 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
}
switch (found) {
.none => {
+ // `.` or `..` from a pane whose directory is gone: said, ENOENT,
+ // not searched for as a word.
+ if (filesystem.dotRelative(trimmed)) {
+ const dir = Pardes.paneDir(pane);
+ var said: [4200]u8 = undefined;
+ return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such directory", .{dir[0..@min(dir.len, 4096)]}) catch "look: no such directory");
+ }
// A raw terminal's clicks are its program's; a look written over
// 9P is no keyboard input, and searches whatever the key mode.
if (pane.image != null or (pane.body.mode == .tty and !p.fs.serving)) return;
@@ -1439,6 +1451,12 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
},
.url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
+ // A directory that is not there (the pane's own, gone): said,
+ // ENOENT, no terminal made in it.
+ if (comptime pardes.hosted) if (filesystem.localPath(dir)) |local| if (!exec.isDirectory(local)) {
+ var said: [4200]u8 = undefined;
+ return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such directory", .{dir[0..@min(dir.len, 4096)]}) catch "look: no such directory");
+ };
for (p.panes, 0..) |slot, i| {
if (slot) |tt| if (std.mem.eql(u8, tt.cwdSlice(), dir) and exec.takesCommandLine(p, i)) {
clearNavigationSelection(p, tt);
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index db37fa49..d9a479fe 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2001,6 +2001,31 @@ test "/index shows a name as the log does: a newline in it is \\n, controls spac
try testing.expectEqual(@as(u64, index.len), call(p, .{ .tag = 1, .op = .getattr, .node = @intFromEnum(tree.TopFile.index) }).reply.attr.size);
}
+test "a served-tree pane spoils no Grep, and Look . from a gone directory is ENOENT, not /virtual/" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "hit.txt", .data = "needle here\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line: [4200]u8 = undefined;
+ _ = wr(p, Node.of(serialOf(p), .name), try std.fmt.bufPrint(&line, "{s}/other.txt\n", .{dir}));
+ // A pane of the served tree.
+ const virtual = try th.newPane(p);
+ _ = wr(p, Node.of(virtual, .name), "/virtual/index\n");
+ // One named the served tree's root, whose directory is `/`.
+ const top = try th.newPane(p);
+ _ = wr(p, Node.of(top, .name), "/virtual\n");
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serialOf(p), .exec), "Grep needle\n").reply.status);
+ // `.` from a pane whose directory is gone.
+ const gone = try th.newPane(p);
+ _ = wr(p, Node.of(gone, .name), "/tmp/pardes-no-such-dir-zz/f.txt\n");
+ const looked = wr(p, Node.of(gone, .look), ".\n");
+ try testing.expectEqual(E.NOENT, looked.errno());
+ try testing.expectStringStartsWith(looked.reply.ename, "look: /tmp/pardes-no-such-dir-zz: no such directory");
+}
+
test "size is monotonic: growing is never refused, and a size once taken is taken again" {
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 });
defer p.deinit();