diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 12:38:59 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 24426b141ebc0db5ee67eb238eeec0f81f6e6ef7 (patch) | |
| tree | e5c828a6a7391eeb523465c13fb248d4c108a470 /src/host_io.zig | |
| parent | e21965c564bf0670c0e95ca493ea69bbc9626577 (diff) | |
| download | pardes-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.zig | 59 |
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 }, |
