summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 12:38:59 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit24426b141ebc0db5ee67eb238eeec0f81f6e6ef7 (patch)
treee5c828a6a7391eeb523465c13fb248d4c108a470 /src/host_io.zig
parente21965c564bf0670c0e95ca493ea69bbc9626577 (diff)
downloadpardes-24426b141ebc0db5ee67eb238eeec0f81f6e6ef7.tar.gz
pardes-24426b141ebc0db5ee67eb238eeec0f81f6e6ef7.zip
A bash or fish under another name gets its prompt marks
The shell's kind was read from its file name alone, so a renamed or differently linked bash got no marks and read busy for ever. A name that says nothing now resolves its links, then looks in the program for bash's or fish's own strings; a POSIX shell's name (sh, even when it is bash) is taken at its word. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig59
1 files changed, 58 insertions, 1 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 072f3100..8bbb0985 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -610,6 +610,63 @@ pub const Shell = struct {
return .none;
}
+ /// `family`, for a shell whose name does not say it (a renamed copy, a
+ /// link named otherwise): what it really is, by the name its links end
+ /// at, else by the program itself -- bash's carries BASH_VERSINFO, fish's
+ /// fish_prompt. ponytail: a string scan of at most 16 MiB, once a spawn
+ /// and only for a name that says nothing; a --version probe would fork.
+ fn familyOf(path: [*:0]const u8) Family {
+ const named = family(std.mem.span(path));
+ if (named != .none) return named;
+ // A name a POSIX shell answers to is that shell, whatever the file:
+ // bash run as `sh` is sh, and reads no --rcfile.
+ const bin = std.mem.span(path);
+ const base = if (std.mem.lastIndexOfScalar(u8, bin, '/')) |s| bin[s + 1 ..] else bin;
+ for ([_][]const u8{ "sh", "dash", "zsh", "ksh", "mksh", "yash", "nu", "elvish", "xonsh", "tcsh", "csh" }) |known|
+ if (std.mem.eql(u8, base, known)) return .none;
+ var real: [std.fs.max_path_bytes]u8 = undefined;
+ if (libc.realpath(path, &real)) |r| {
+ const by_link = family(std.mem.span(@as([*:0]const u8, @ptrCast(r))));
+ if (by_link != .none) return by_link;
+ }
+ const fd = libc.open(path, .{ .ACCMODE = .RDONLY }, @as(libc.mode_t, 0));
+ if (fd < 0) return .none;
+ defer _ = libc.close(fd);
+ var chunk: [64 * 1024]u8 = undefined;
+ var carry: usize = 0;
+ var total: usize = 0;
+ while (total < 16 << 20) {
+ const n = libc.read(fd, chunk[carry..].ptr, chunk.len - carry);
+ if (n <= 0) break;
+ const have = carry + @as(usize, @intCast(n));
+ total += @intCast(n);
+ // A script is no binary: its interpreter is what runs.
+ if (total == have and std.mem.startsWith(u8, chunk[0..have], "#!")) return .none;
+ if (std.mem.indexOf(u8, chunk[0..have], "BASH_VERSINFO") != null) return .bash;
+ if (std.mem.indexOf(u8, chunk[0..have], "fish_prompt") != null) return .fish;
+ // Keep the tail, so a marker cut by the chunk is still seen.
+ carry = @min(have, 16);
+ std.mem.copyForwards(u8, chunk[0..carry], chunk[have - carry .. have]);
+ }
+ return .none;
+ }
+
+ test "a bash under another name is still told as bash" {
+ const bash: [:0]const u8 = "/usr/bin/bash";
+ if (libc.access(bash, X_OK) != 0) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const bytes = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, bash, std.testing.allocator, .limited(16 << 20));
+ defer std.testing.allocator.free(bytes);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "mysh", .data = bytes, .flags = .{ .permissions = .executable_file } });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ var path: [4200:0]u8 = undefined;
+ const p = try std.fmt.bufPrintZ(&path, "{s}/mysh", .{dir});
+ try std.testing.expectEqual(Family.bash, familyOf(p.ptr));
+ try std.testing.expectEqual(Family.none, familyOf("/bin/sh"));
+ }
+
const bash_rc =
\\[ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc"
\\PS1='\[\e]133;A;cl=line;aid=pardes\a\]'"$PS1"'\[\e]133;B;aid=pardes\a\]'
@@ -801,7 +858,7 @@ pub const Shell = struct {
// Resolve in the parent. The path buffer and prompt files must survive through exec.
pub fn resolve(bin: []const u8, buf: *[std.fs.max_path_bytes]u8, prompt_rcs: *const PromptFiles) Spawn {
const path = find(bin, buf) orelse fallback(buf);
- const marks: [2]?[*:0]const u8 = switch (family(std.mem.span(path))) {
+ const marks: [2]?[*:0]const u8 = switch (familyOf(path)) {
.bash => if (prompt_rcs.bashPath()) |rc| .{ "--rcfile", rc.ptr } else .{ null, null },
.fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null },
.none => .{ null, null },