diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /src/shell_bin.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/shell_bin.zig')
| -rw-r--r-- | src/shell_bin.zig | 153 |
1 files changed, 139 insertions, 14 deletions
diff --git a/src/shell_bin.zig b/src/shell_bin.zig index ecd80937..81ff90fa 100644 --- a/src/shell_bin.zig +++ b/src/shell_bin.zig @@ -9,6 +9,11 @@ //! keeping a copy each. Nothing here imports the core, which is also what lets //! it be its own std-only test module. //! +//! Each host owns one `PromptRcs` for its lifetime. Its files are private +//! `mkstemp` names, completely written and closed before resolve can expose +//! them to a child. Concurrent launches therefore share neither a pathname nor +//! an inode, and a shell can never source another user's predictable /tmp file. +//! //! ALL OF THIS RUNS IN THE PARENT. Between fork and exec a process may not //! allocate, and a $PATH search does — which is the same reason the exec is //! `execv` on an absolute path and never `execvp`. So the lookup is a handful @@ -21,6 +26,8 @@ const builtin = @import("builtin"); const libc = std.c; const X_OK: c_int = 1; +extern "c" fn mkstemp(template: [*:0]u8) c_int; + /// Prompt integration, per shell FAMILY rather than per binary: pardes hides /// prompt rows, moves the cursor by clicking one, and tells a command's output /// from the line that asked for it, and all three read the OSC 133 marks a @@ -54,10 +61,7 @@ pub fn shellRc(bin: []const u8) ShellRc { return .none; } -pub const bash_rc_path = "/tmp/pardes-osc133.bash"; -pub const fish_rc_path = "/tmp/pardes-osc133.fish"; - -pub const bash_rc = +const bash_rc = \\[ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" \\PS1='\[\e]133;A;cl=line\a\]'"$PS1"'\[\e]133;B\a\]' \\PROMPT_COMMAND='printf "\e]133;D\a"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}" @@ -78,7 +82,7 @@ pub const bash_rc = /// C and D come off fish's own `fish_preexec`/`fish_postexec` events rather /// than being spliced into the prompt, which is what bash's DEBUG trap is /// working around. -pub const fish_rc = +const fish_rc = \\functions -c fish_prompt __pardes_user_prompt \\function fish_prompt \\ printf '\e]133;A;cl=line\a' @@ -94,6 +98,87 @@ pub const fish_rc = \\ ; +const rc_path_capacity = 64; + +/// The two complete, private prompt files a native host lends to every shell +/// it spawns. No allocation and no global name: moving this value is safe +/// because it stores lengths, never pointers into its own buffers. +pub const PromptRcs = struct { + bash_path: [rc_path_capacity:0]u8 = @splat(0), + bash_len: u8 = 0, + fish_path: [rc_path_capacity:0]u8 = @splat(0), + fish_len: u8 = 0, + fish_command: [rc_path_capacity + "source ".len:0]u8 = @splat(0), + fish_command_len: u8 = 0, + + pub fn init() PromptRcs { + var rcs: PromptRcs = .{}; + rcs.bash_len = stage(&rcs.bash_path, "/tmp/pardes-osc133-bash-XXXXXX", bash_rc); + rcs.fish_len = stage(&rcs.fish_path, "/tmp/pardes-osc133-fish-XXXXXX", fish_rc); + if (rcs.fishPath()) |path| { + const command = std.fmt.bufPrintSentinel(&rcs.fish_command, "source {s}", .{path}, 0) catch { + _ = libc.unlink(path.ptr); + rcs.fish_len = 0; + return rcs; + }; + rcs.fish_command_len = @intCast(command.len); + } + return rcs; + } + + pub fn deinit(rcs: *PromptRcs) void { + if (rcs.bashPath()) |path| _ = libc.unlink(path.ptr); + if (rcs.fishPath()) |path| _ = libc.unlink(path.ptr); + rcs.bash_len = 0; + rcs.fish_len = 0; + rcs.fish_command_len = 0; + } + + fn bashPath(rcs: *const PromptRcs) ?[:0]const u8 { + if (rcs.bash_len == 0) return null; + return rcs.bash_path[0..rcs.bash_len :0]; + } + + fn fishPath(rcs: *const PromptRcs) ?[:0]const u8 { + if (rcs.fish_len == 0) return null; + return rcs.fish_path[0..rcs.fish_len :0]; + } + + fn fishCommand(rcs: *const PromptRcs) ?[:0]const u8 { + if (rcs.fish_command_len == 0) return null; + return rcs.fish_command[0..rcs.fish_command_len :0]; + } +}; + +/// Create one private 0600 file and reveal its length only after the complete +/// write and close. Failure leaves no pathname for resolve to hand to a shell. +fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u8 { + const path = std.fmt.bufPrintSentinel(path_buf, "{s}", .{template}, 0) catch return 0; + const fd = mkstemp(path.ptr); + if (fd < 0) return 0; + var off: usize = 0; + while (off < contents.len) { + const n = libc.write(fd, contents[off..].ptr, contents.len - off); + if (n < 0) { + if (libc.errno(n) == .INTR) continue; + _ = libc.close(fd); + _ = libc.unlink(path.ptr); + return 0; + } + if (n == 0) { + _ = libc.close(fd); + _ = libc.unlink(path.ptr); + return 0; + } + off += @intCast(n); + } + if (libc.close(fd) != 0) { + _ = libc.unlink(path.ptr); + return 0; + } + return @intCast(path.len); +} + test "shell family is the basename's prefix, and anything else runs unadorned" { try std.testing.expectEqual(ShellRc.fish, shellRc("fish")); try std.testing.expectEqual(ShellRc.fish, shellRc("/usr/bin/fish")); @@ -143,17 +228,18 @@ pub const Spawn = struct { /// `bin` is whatever the Shell builtin was given — a bare name to look up, or /// a path (anything with a `/`) to take at its word. `buf` holds the resolved /// path for as long as the returned Spawn is used, which for a caller that is -/// about to fork means: until the child execs. -pub fn resolve(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) Spawn { +/// about to fork means: until the child execs. `prompt_rcs` is host-lifetime +/// storage and must likewise remain alive through that exec. +pub fn resolve(bin: []const u8, buf: *[std.fs.max_path_bytes]u8, prompt_rcs: *const PromptRcs) Spawn { const path = find(bin, buf) orelse fallback(buf); // the family comes off the path that will ACTUALLY be executed, not the // name that was asked for — `Shell sh` on a system where that is a symlink // to bash still has no `--rcfile` promise attached to it, and a resolved // /usr/bin/fish reads as fish whether it was reached by name or by path const marks: [2]?[*:0]const u8 = switch (shellRc(std.mem.span(path))) { - .bash => .{ "--rcfile", bash_rc_path }, + .bash => if (prompt_rcs.bashPath()) |rc| .{ "--rcfile", rc.ptr } else .{ null, null }, // -C runs AFTER config.fish, which is the whole point (see fish_rc) - .fish => .{ "-C", "source " ++ fish_rc_path }, + .fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null }, .none => .{ null, null }, }; return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } }; @@ -194,28 +280,67 @@ fn fallback(buf: *[std.fs.max_path_bytes]u8) [*:0]const u8 { test "a path is taken at its word, a name is looked up, and both pick their own marks" { if (builtin.os.tag == .windows) return; var buf: [std.fs.max_path_bytes]u8 = undefined; + var prompt_rcs = PromptRcs.init(); + defer prompt_rcs.deinit(); // /bin/sh exists on every unix this builds for and is in no family, so it // pins the resolve-by-path arm AND the unadorned argv - const sh = resolve("/bin/sh", &buf); + const sh = resolve("/bin/sh", &buf, &prompt_rcs); try std.testing.expectEqualStrings("/bin/sh", std.mem.span(sh.path)); try std.testing.expect(sh.argv[1] == null); // a name with no slash is searched for; whatever it resolves to, it is a // bash and so carries --rcfile pointing at the rc the shells write - const bash = resolve("bash", &buf); + const bash = resolve("bash", &buf, &prompt_rcs); try std.testing.expect(shellRc(std.mem.span(bash.path)) == .bash); try std.testing.expectEqualStrings("--rcfile", std.mem.span(bash.argv[1].?)); - try std.testing.expectEqualStrings(bash_rc_path, std.mem.span(bash.argv[2].?)); + try std.testing.expectEqualStrings(prompt_rcs.bashPath().?, std.mem.span(bash.argv[2].?)); // nothing is installed under this name, so the fallback answers — and the // fallback is a real executable, not the name that failed - const missing = resolve("zznosuchshell", &buf); + const missing = resolve("zznosuchshell", &buf, &prompt_rcs); try std.testing.expect(!std.mem.eql(u8, "zznosuchshell", std.mem.span(missing.path))); try std.testing.expect(libc.access(missing.path, X_OK) == 0); // an absolute path that does not exist falls back too, rather than being // handed to exec to fail on - const gone = resolve("/zz/no/such/shell", &buf); + const gone = resolve("/zz/no/such/shell", &buf, &prompt_rcs); try std.testing.expect(libc.access(gone.path, X_OK) == 0); } + +test "prompt rc owners have private complete files and clean them up" { + if (builtin.os.tag == .windows) return; + var a = PromptRcs.init(); + defer a.deinit(); + var b = PromptRcs.init(); + defer b.deinit(); + const a_bash = a.bashPath() orelse return error.TempCreateFailed; + const b_bash = b.bashPath() orelse return error.TempCreateFailed; + const a_fish = a.fishPath() orelse return error.TempCreateFailed; + try std.testing.expect(!std.mem.eql(u8, a_bash, b_bash)); + const fish_command = a.fishCommand() orelse return error.MissingFishCommand; + try std.testing.expectEqualStrings("source ", fish_command[0.."source ".len]); + try std.testing.expectEqualStrings(a_fish, fish_command["source ".len..]); + + var buf: [bash_rc.len]u8 = undefined; + const fd = libc.open(a_bash.ptr, .{ .ACCMODE = .RDONLY }); + if (fd < 0) return error.OpenFailed; + defer _ = libc.close(fd); + var len: usize = 0; + while (len < buf.len) { + const n = libc.read(fd, buf[len..].ptr, buf.len - len); + if (n < 0) { + if (libc.errno(n) == .INTR) continue; + return error.ReadFailed; + } + if (n == 0) break; + len += @intCast(n); + } + try std.testing.expectEqualStrings(bash_rc, buf[0..len]); + + var removed: [rc_path_capacity:0]u8 = @splat(0); + @memcpy(removed[0..a_bash.len], a_bash); + removed[a_bash.len] = 0; + a.deinit(); + try std.testing.expect(libc.access(&removed, 0) < 0); +} |
