From e9eae368dbbe855ad2f409913ff0d44d5ac32bd8 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 05:22:35 -0300 Subject: Tty takes a directory as long as a path may be A terminal's directory rode inline in the spawn effect, capped at 256 bytes, so Tty in a directory of about 377 bytes said path too long. A directory past the inline buffer now goes out of line (Pardes.spawn_dirs, given to the host when the spawn is performed), up to the host's path limit (4095); past that, Tty says its directory is longer than a path may be. The effect queue stays its size. Co-Authored-By: Claude Opus 5.5 --- src/dump.zig | 2 +- src/exec.zig | 6 +++++- src/ninep/pty.zig | 2 +- src/pardes.zig | 36 +++++++++++++++++++++++++++++------- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/dump.zig b/src/dump.zig index 54b77451..7b27876e 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -848,7 +848,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo restored.command_status = t.status; } if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty; - if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } }); + if (revive) p.emitSpawn(i, restored.serial, t.cwd); break :terminal restored; }, .file => try pardes.panes.File.restore(p, i, src), diff --git a/src/exec.zig b/src/exec.zig index 73413910..c2d1be21 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -348,6 +348,10 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { return null; }; const nt = p.newShell(free, p.execDir(from)) catch |err| { + if (err == error.PathTooLong) { + p.reportFailure(from, std.fmt.comptimePrint("Tty: its directory is longer than a path may be ({d} bytes)", .{pardes.memory.limits.host_path_cap})); + return null; + } p.reportError(from, "Tty", err); return null; }; @@ -663,7 +667,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l"); panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[0m"); echoCommand(p, pane, line); - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); + p.emitSpawn(id, pane.serial, pane.cwdSlice()); noteRun(p, pane, "run", line); return id; } diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 6b2abd0a..046a456a 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -87,7 +87,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { if (words.next() != null) return false; if (pane.cwdSlice().len > pardes.effect_path_cap) return false; if (!apply) return true; - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); + p.emitSpawn(id, pane.serial, pane.cwdSlice()); }, } return true; diff --git a/src/pardes.zig b/src/pardes.zig index 9f192004..a55a22e0 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2139,10 +2139,19 @@ test "owned cwd preserves long paths aliases and failed updates" { const original = pane.cwdSlice(); try pane.setOwnedCwd(original); try std.testing.expectEqualStrings(&path, pane.cwdSlice()); - const effects = p.effects_len; + // A terminal's directory may be as long as a path is (PATH_MAX): one + // past the effect's buffer goes out of line (spawn_dirs); only one past + // the host's path limit is too long. const free = p.freeSlot().?; - try std.testing.expectError(error.PathTooLong, p.newShell(free, path[0 .. effect_path_cap + 1])); + _ = try p.newShell(free, path[0..]); + try std.testing.expectEqualStrings(&path, p.spawn_dirs[free].?); + try p.removePane(free, null); + while (p.nextEffect()) |_| {} + var too_long: [limits.host_path_cap + 1]u8 = @splat('d'); + too_long[0] = '/'; + try std.testing.expectError(error.PathTooLong, p.newShell(free, &too_long)); try std.testing.expect(p.panes[free] == null); + const effects = p.effects_len; exec.saveTo(p, 0, "file.txt"); try std.testing.expectEqual(effects, p.effects_len); try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "path too long") != null); @@ -4304,6 +4313,9 @@ pub const Pardes = struct { /// (`sessionDir`), as acme's row and column tags do. exec_header: bool = false, session_dir: [limits.host_path_cap]u8 = undefined, + /// A spawn's directory too long for the effect's inline buffer, by pane + /// slot: the effect then carries none, and perform gives the host this. + spawn_dirs: [MAX_PANES]?[]u8 = @splat(null), session_dir_len: ?u16 = null, /// acme's activecol (dat.c:37), by column serial, 0 for none: the /// column last typed in (acme.c:487), clicked in with the select button @@ -4647,6 +4659,7 @@ pub const Pardes = struct { if (p.last_dump) |d| gpa.free(d); if (p.custom_theme) |theme_value| std.zon.parse.free(gpa, theme_value); if (p.chord_arg) |a| gpa.free(a); + for (p.spawn_dirs) |d| if (d) |dir| gpa.free(dir); if (p.pipe.wait) |*wait| wait.deinit(gpa); p.locations_cache.deinit(p.tree_sitter_gpa); p.fs.deinit(gpa); @@ -4789,20 +4802,29 @@ pub const Pardes = struct { pub fn newShell(p: *Pardes, id: usize, cwd: []const u8) !*Pane { std.debug.assert(p.panes[id] == null); - if (cwd.len > effect_path_cap) return error.PathTooLong; + if (cwd.len > limits.host_path_cap) return error.PathTooLong; const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h); panes.Terminal.armShellSpawn(pane); p.installPane(id, pane); - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } }); + p.emitSpawn(id, pane.serial, cwd); return pane; } + /// A child for pane `id` in `cwd`: a directory up to PATH_MAX, the ones + /// past the effect's inline buffer kept here for perform to give. + pub fn emitSpawn(p: *Pardes, id: usize, serial: u32, cwd: []const u8) void { + if (p.spawn_dirs[id]) |old| p.gpa.free(old); + p.spawn_dirs[id] = null; + if (cwd.len > effect_path_cap) p.spawn_dirs[id] = p.gpa.dupe(u8, cwd) catch null; + p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = serial, .cwd = .from(if (cwd.len > effect_path_cap) "" else cwd) } }); + } + /// A command pane: a terminal whose child is `$SHELL -c line` rather /// than an interactive shell, left showing its output and `exit N` when /// it is done (exec.zig runCommand). pub fn newCommand(p: *Pardes, id: usize, cwd: []const u8, line: []const u8) !*Pane { std.debug.assert(p.panes[id] == null); - if (cwd.len > effect_path_cap) return error.PathTooLong; + if (cwd.len > limits.host_path_cap) return error.PathTooLong; const owned = try p.gpa.dupe(u8, line); errdefer p.gpa.free(owned); const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h); @@ -4813,7 +4835,7 @@ pub const Pardes = struct { // directory's commands reuse it; the child's own cd does not move it. pane.setOwnedCwd(cwd) catch {}; p.installPane(id, pane); - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } }); + p.emitSpawn(id, pane.serial, cwd); return pane; } @@ -5225,7 +5247,7 @@ pub const Pardes = struct { switch (e) { // The host reads the pane (its command, its shell) as it forks, // so the slot must still hold the pane the spawn was for. - .spawn => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.spawn) |f| f(p.host.ctx, s.pane, s.cwd.slice()) else { + .spawn => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.spawn) |f| f(p.host.ctx, s.pane, if (s.cwd.len == 0) (p.spawn_dirs[s.pane] orelse "") else s.cwd.slice()) else { p.fallback.spawned[s.pane] = true; }, // A pane with no child is silent: nothing invents output on its -- cgit v1.3