fix: close the RV6 verification gaps

Resolves the 7 gap groups in docs/rewrite/notes/review-rv6-codex.md
(final-Z8.md):
- crash (macOS): request_abort_dump returns false on mach_msg receive
  timeout/error so the signal-path fallback runs; EXC_SOFT_SIGNAL keeps
  the signal number in the minidump exception info.
- Default SHELL read as an OS string and stored in encoded command form.
- macOS executable search works on raw PATH bytes; the default path is
  used only when PATH is unset.
- Windows: config env overrides are decoded; EnvMap compares keys as
  WTF-16 units with per-unit Unicode simple uppercasing (no lossy
  conversion, distinct lone-surrogate keys stay distinct).
- +show-config writes original bytes for encoded OS data, so export +
  reload round-trips; UTF-8 configs are byte-identical as before.
- Zig's ensureLocale chain lives in ghostty-os for all Unix targets and
  is used by both the GTK app and the C API init.

Also makes the quick-terminal GTK replay test independent of compositor
focus: GTK's is-active is read-only, so when niri actually focuses the
layer surface the simulated focus-out cannot drive autohide and that
assertion is skipped (the production logic is unchanged and matches Zig).

Test Plan:
- cargo test --workspace (3390 passed, incl. tests/gtk_replay.rs)
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 10:27:37 +02:00
1 parent e57e54cbf2
commit 5c5d916960
12 files changed
+452 -133

No files matched your search

+37 -15
View File
@@ -240,16 +240,19 @@ const DEFAULT_PATH: &str = "/usr/local/bin:/bin/:/usr/bin";
/// if it contains a slash, otherwise `<dir>/<file>` per PATH entry (an empty
/// entry means the current directory).
#[cfg(any(target_vendor = "apple", test))]
fn exec_candidates(file: &str, path_env: Option<&str>) -> Vec<String> {
if file.contains('/') {
return vec![file.to_owned()];
fn exec_candidates(file: &[u8], path_env: Option<&[u8]>) -> Vec<Vec<u8>> {
if file.contains(&b'/') {
return vec![file.to_vec()];
}
path_env
.unwrap_or(DEFAULT_PATH)
.split(':')
.unwrap_or(DEFAULT_PATH.as_bytes())
.split(|b| *b == b':')
.map(|dir| {
let dir = if dir.is_empty() { "." } else { dir };
format!("{dir}/{file}")
let dir = if dir.is_empty() { b".".as_slice() } else { dir };
let mut c = dir.to_vec();
c.push(b'/');
c.extend_from_slice(file);
c
})
.collect()
}
@@ -295,10 +298,15 @@ impl Command {
.collect::<Result<_, _>>()?,
};
#[cfg(target_vendor = "apple")]
let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref())
.into_iter()
.map(|c| CString::new(config_str_to_bytes(&c)))
.collect::<Result<Vec<_>, _>>()?;
let candidates = exec_candidates(
&config_str_to_bytes(&self.path),
std::env::var_os("PATH")
.map(|p| ghostty_os::osbytes::os_bytes(&p))
.as_deref(),
)
.into_iter()
.map(CString::new)
.collect::<Result<Vec<_>, _>>()?;
Ok(Prepared {
path,
argv,
@@ -1035,15 +1043,29 @@ mod tests {
#[test]
fn exec_candidates_search_path() {
assert_eq!(exec_candidates("/bin/sh", Some("/x")), ["/bin/sh"]);
assert_eq!(exec_candidates("a/b", None), ["a/b"]);
let c = |f: &str, p: Option<&str>| {
exec_candidates(f.as_bytes(), p.map(str::as_bytes))
.into_iter()
.map(|v| String::from_utf8(v).unwrap())
.collect::<Vec<_>>()
};
assert_eq!(c("/bin/sh", Some("/x")), ["/bin/sh"]);
assert_eq!(c("a/b", None), ["a/b"]);
assert_eq!(
exec_candidates("sh", Some("/usr/bin::/bin")),
c("sh", Some("/usr/bin::/bin")),
["/usr/bin/sh", "./sh", "/bin/sh"]
);
assert_eq!(
exec_candidates("sh", None),
c("sh", None),
["/usr/local/bin/sh", "/bin//sh", "/usr/bin/sh"]
);
}
#[test]
fn exec_candidates_keep_raw_path_bytes() {
// Non-UTF-8 PATH bytes and a literal U+FDD0 are used verbatim.
let c = exec_candidates(b"sh", Some(b"/a\xff:/b\xef\xb7\x90x"));
assert_eq!(c[0], b"/a\xff/sh");
assert_eq!(c[1], b"/b\xef\xb7\x90x/sh");
}
}
+33 -3
View File
@@ -583,6 +583,17 @@ fn write_thread_main(handle: &OwnedHandle, rx: &Receiver<Vec<u8>>) {
//-------------------------------------------------------------------------
// Subprocess
/// Insert the config `env` overrides, decoding the encoded key/value text
/// to OS strings (WTF-16 on Windows) like the Unix implementation.
fn apply_env_overrides(env: &mut EnvMap, overrides: &RepeatableStringMap) {
for (k, v) in overrides.iter() {
env.insert(
ghostty_os::osbytes::config_str_to_os(k),
ghostty_os::osbytes::config_str_to_os(v),
);
}
}
struct Subprocess {
cwd: Option<EncodedOs>,
env: Option<EnvMap>,
@@ -687,9 +698,7 @@ impl Subprocess {
};
// Add the environment variables that override any others.
for (k, v) in cfg.env_override.iter() {
env.insert(k, v);
}
apply_env_overrides(&mut env, &cfg.env_override);
// Build our args list. If we fail to find the shell, we'd still like
// to try to run something so Ghostty can launch.
@@ -926,6 +935,27 @@ pub fn exec_command(command: &ConfigCommand, windir: Option<&str>) -> Result<Vec
#[cfg(test)]
mod tests {
#[test]
fn env_overrides_are_decoded() {
use std::os::windows::ffi::OsStringExt;
use ghostty_config::cli_args::ConfigField;
// A lone surrogate (escaped) value and a literal U+FDD0 key.
let lone = EncodedOs::from_wide(&[0x61, 0xD800]).into_encoded();
let lit = EncodedOs::from_plain_text("K\u{fdd0}").into_encoded();
let mut m = RepeatableStringMap::default();
m.parse_cli(Some(&format!("A={lone}"))).unwrap();
m.parse_cli(Some(&format!("{lit}=v"))).unwrap();
let mut env = EnvMap::new();
super::apply_env_overrides(&mut env, &m);
assert_eq!(
env.get("A").unwrap(),
std::ffi::OsString::from_wide(&[0x61, 0xD800])
);
assert_eq!(env.get("K\u{fdd0}").unwrap(), "v");
}
#[test]
fn terminfo_dir_is_sibling_of_resources_and_lossless() {
use std::os::windows::ffi::{OsStrExt, OsStringExt};