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:
1 parent
306a19227f
commit
5e5d33324c
12 files changed
+452
-133
No files matched your search
@@ -12,6 +12,31 @@ const DELIMITER: char = ';';
|
||||
#[cfg(not(windows))]
|
||||
const DELIMITER: char = ':';
|
||||
|
||||
/// Uppercase one UTF-16 code unit like Windows' `RtlUpcaseUnicodeChar`
|
||||
/// (Unicode simple case mapping, per unit): surrogate halves and characters
|
||||
/// whose uppercase is not a single BMP code unit are unchanged.
|
||||
fn upcase_unit(u: u16) -> u16 {
|
||||
let Some(c) = char::from_u32(u32::from(u)) else {
|
||||
return u; // lone surrogate half
|
||||
};
|
||||
let mut up = c.to_uppercase();
|
||||
match (up.next(), up.next()) {
|
||||
(Some(m), None) => u16::try_from(u32::from(m)).unwrap_or(u),
|
||||
_ => u,
|
||||
}
|
||||
}
|
||||
|
||||
/// The comparison form of an environment key on Windows: its WTF-16 code
|
||||
/// units, uppercased per unit. Never lossy: distinct lone surrogates stay
|
||||
/// distinct.
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn windows_key(key: &OsStr) -> Vec<u16> {
|
||||
crate::osbytes::wtf8_to_wide(&crate::osbytes::os_bytes(key))
|
||||
.into_iter()
|
||||
.map(upcase_unit)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A byte-preserving environment map (Zig `std.process.EnvMap`): keys and
|
||||
/// values are arbitrary OS strings, never required to be UTF-8.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
@@ -28,10 +53,10 @@ impl EnvMap {
|
||||
/// itself.
|
||||
#[cfg(windows)]
|
||||
fn canonical_key<'a>(&'a self, key: &'a OsStr) -> Cow<'a, OsStr> {
|
||||
let want = key.to_string_lossy().to_ascii_uppercase();
|
||||
let want = windows_key(key);
|
||||
self.0
|
||||
.keys()
|
||||
.find(|k| k.to_string_lossy().to_ascii_uppercase() == want)
|
||||
.find(|k| windows_key(k) == want)
|
||||
.map_or(Cow::Borrowed(key), |k| Cow::Borrowed(k.as_os_str()))
|
||||
}
|
||||
|
||||
@@ -372,3 +397,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod windows_key_tests {
|
||||
use super::*;
|
||||
|
||||
fn key(units: &[u16]) -> Vec<u16> {
|
||||
units.iter().copied().map(upcase_unit).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upcase_is_unicode_simple_per_unit() {
|
||||
assert_eq!(upcase_unit(u16::from(b'a')), u16::from(b'A'));
|
||||
assert_eq!(upcase_unit(0x00E9), 0x00C9); // e acute
|
||||
assert_eq!(upcase_unit(0x03C9), 0x03A9); // omega
|
||||
assert_eq!(upcase_unit(0x00DF), 0x00DF); // sharp s has no simple upper
|
||||
assert_eq!(upcase_unit(0xD800), 0xD800);
|
||||
assert_eq!(upcase_unit(0xDFFF), 0xDFFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii_case_keys_are_equal() {
|
||||
assert_eq!(key(&[0xE9, 0x61]), key(&[0xC9, 0x41]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_lone_surrogate_keys_stay_distinct() {
|
||||
assert_ne!(key(&[0x41, 0xD800]), key(&[0x41, 0xD801]));
|
||||
assert_ne!(key(&[0xD800]), key(&[0xFFFD]));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn windows_key_of_os_str_is_wtf16() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
// Unix OsStr "Aé" in UTF-8.
|
||||
assert_eq!(
|
||||
windows_key(OsStr::from_bytes("a\u{e9}".as_bytes())),
|
||||
vec![0x41, 0xC9]
|
||||
);
|
||||
}
|
||||
}
|
||||
+175
-49
@@ -1,47 +1,29 @@
|
||||
//! Locale setup (`os/locale.zig`).
|
||||
//!
|
||||
//! DEFERRED: the Zig `ensureLocale` is built from `setlocale(LC_ALL, "")`
|
||||
//! plus `setenv`/`unsetenv` of `LANG`. All of these are `unsafe` in Rust
|
||||
//! (`libc::setlocale`, `gettextrs::setlocale` and Rust 2024's
|
||||
//! `std::env::set_var`) and this crate forbids `unsafe`. There is no safe
|
||||
//! crate that wraps `setlocale`.
|
||||
//!
|
||||
//! Resolution options for integration:
|
||||
//! * GTK (`gtk_init`, called by `gtk4::init()`) already calls
|
||||
//! `setlocale(LC_ALL, "")` on Linux, so for the GTK apprt nothing else is
|
||||
//! needed for gettext/Pango to pick up the locale.
|
||||
//! * If an explicit call is needed (e.g. CLI actions without GTK), an
|
||||
//! `unsafe`-permitted crate (apprt-gtk / the root binary) must perform the
|
||||
//! libc call; [`FALLBACK_LOCALE`] documents the Zig fallback behaviour
|
||||
//! (try `setlocale(LC_ALL, "")`, then with `LANG` unset, then
|
||||
//! `en_US.UTF-8` and export `LANG=en_US.UTF-8`).
|
||||
//!
|
||||
//! On Darwin (`target_vendor = "apple"`) the full Zig behaviour is
|
||||
//! implemented in [`darwin`] (it needs `unsafe` for `setlocale`/`setenv`):
|
||||
//! `LANG`/`LANGUAGE` are pre-populated from Cocoa when unset and
|
||||
//! `setlocale(LC_ALL, "")` is called with the Zig fallback chain.
|
||||
//! On every Unix target the full Zig behaviour is implemented in [`posix`]
|
||||
//! (it needs `unsafe` for `setlocale`/`setenv`, allowed there with `SAFETY`
|
||||
//! comments): `setlocale(LC_ALL, "")`, then with `LANG` unset, then
|
||||
//! `en_US.UTF-8` (exported as `LANG`). On Darwin (`target_vendor = "apple"`)
|
||||
//! `LANG`/`LANGUAGE` are additionally pre-populated from Cocoa when unset.
|
||||
//! Other targets (Windows, wasm) have no `setlocale` handling.
|
||||
|
||||
/// The locale Ghostty falls back to (and exports as `LANG`) when the
|
||||
/// environment's locale can't be set.
|
||||
pub const FALLBACK_LOCALE: &str = "en_US.UTF-8";
|
||||
|
||||
/// Ensure that the locale is set.
|
||||
///
|
||||
/// DEFERRED (see module docs): this is currently a no-op that only logs.
|
||||
/// Returns `Some(value)` when `LANG` should be set to `value` for child
|
||||
/// processes; always `None` for now.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub fn ensure_locale() -> Option<String> {
|
||||
log::debug!("ensure_locale: setlocale is not available from safe Rust; relying on the toolkit");
|
||||
None
|
||||
}
|
||||
|
||||
/// Ensure that the locale is set (Darwin: full port of `ensureLocale`).
|
||||
/// Ensure that the locale is set (`ensureLocale`).
|
||||
/// Returns `Some("en_US.UTF-8")` if we had to fall back to it (and exported
|
||||
/// it as `LANG`), otherwise `None`.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[cfg(unix)]
|
||||
pub fn ensure_locale() -> Option<String> {
|
||||
darwin::ensure_locale()
|
||||
posix::ensure_locale()
|
||||
}
|
||||
|
||||
/// Ensure that the locale is set: nothing to do on targets without libc
|
||||
/// locales (Windows, wasm).
|
||||
#[cfg(not(unix))]
|
||||
pub fn ensure_locale() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Format the system locale as `<lang>_<country>.UTF-8` (the value Zig's
|
||||
@@ -71,12 +53,14 @@ pub fn preferred_language_value<S: AsRef<str>>(preferred: &[S]) -> Option<String
|
||||
(!out.is_empty()).then_some(out)
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[cfg(unix)]
|
||||
#[allow(unsafe_code)]
|
||||
mod darwin {
|
||||
mod posix {
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
use super::{FALLBACK_LOCALE, format_lang, preferred_language_value};
|
||||
use super::FALLBACK_LOCALE;
|
||||
#[cfg(target_vendor = "apple")]
|
||||
use super::{format_lang, preferred_language_value};
|
||||
|
||||
fn setlocale_all(locale: &str) -> Option<String> {
|
||||
let c = CString::new(locale).ok()?;
|
||||
@@ -100,6 +84,7 @@ mod darwin {
|
||||
}
|
||||
|
||||
/// Sets LANG (and LANGUAGE) from the system preferences.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn set_lang_from_cocoa() {
|
||||
use objc2_foundation::NSLocale;
|
||||
|
||||
@@ -129,18 +114,48 @@ mod darwin {
|
||||
}
|
||||
}
|
||||
|
||||
/// The process state `ensureLocale` reads and updates (a trait so the
|
||||
/// fallback chain is testable without touching the real process).
|
||||
pub(super) trait Sys {
|
||||
fn lang(&self) -> Option<std::ffi::OsString>;
|
||||
fn set_lang(&mut self, value: &str);
|
||||
fn unset_lang(&mut self);
|
||||
fn setlocale(&mut self, locale: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
struct Process;
|
||||
|
||||
impl Sys for Process {
|
||||
fn lang(&self) -> Option<std::ffi::OsString> {
|
||||
std::env::var_os("LANG")
|
||||
}
|
||||
fn set_lang(&mut self, value: &str) {
|
||||
set_env("LANG", value);
|
||||
}
|
||||
fn unset_lang(&mut self) {
|
||||
unset_env("LANG");
|
||||
}
|
||||
fn setlocale(&mut self, locale: &str) -> Option<String> {
|
||||
setlocale_all(locale)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ensure_locale() -> Option<String> {
|
||||
// On macOS, pre-populate the LANG env var with system preferences.
|
||||
// When launching the .app, LANG is not set so we must query it from
|
||||
// the OS. When launching from the CLI, LANG is usually set by the
|
||||
// parent process.
|
||||
let lang = std::env::var_os("LANG");
|
||||
if lang.as_ref().is_none_or(|v| v.is_empty()) {
|
||||
#[cfg(target_vendor = "apple")]
|
||||
if std::env::var_os("LANG").is_none_or(|v| v.is_empty()) {
|
||||
set_lang_from_cocoa();
|
||||
}
|
||||
|
||||
ensure_locale_with(&mut Process)
|
||||
}
|
||||
|
||||
pub(super) fn ensure_locale_with(sys: &mut impl Sys) -> Option<String> {
|
||||
// Set the locale to whatever is set in env vars.
|
||||
if let Some(v) = setlocale_all("") {
|
||||
if let Some(v) = sys.setlocale("") {
|
||||
log::info!("setlocale from env result={v}");
|
||||
return None;
|
||||
}
|
||||
@@ -148,13 +163,13 @@ mod darwin {
|
||||
// setlocale failed. This is probably because the LANG env var is
|
||||
// invalid. Try to set it without the LANG var set to use the system
|
||||
// default.
|
||||
if std::env::var_os("LANG").is_some_and(|v| !v.is_empty()) {
|
||||
if sys.lang().is_some_and(|v| !v.is_empty()) {
|
||||
// We don't need to do both of these things but we do them both
|
||||
// to be sure that lang is either empty or unset completely.
|
||||
set_env("LANG", "");
|
||||
unset_env("LANG");
|
||||
sys.set_lang("");
|
||||
sys.unset_lang();
|
||||
|
||||
if let Some(v) = setlocale_all("") {
|
||||
if let Some(v) = sys.setlocale("") {
|
||||
log::info!("setlocale after unset lang result={v}");
|
||||
|
||||
// If we try to setlocale to an unsupported locale it'll
|
||||
@@ -170,14 +185,127 @@ mod darwin {
|
||||
log::warn!(
|
||||
"setlocale failed with LANG and system default. Falling back to {FALLBACK_LOCALE}"
|
||||
);
|
||||
if let Some(v) = setlocale_all(FALLBACK_LOCALE) {
|
||||
set_env("LANG", FALLBACK_LOCALE);
|
||||
if let Some(v) = sys.setlocale(FALLBACK_LOCALE) {
|
||||
sys.set_lang(FALLBACK_LOCALE);
|
||||
log::info!("setlocale default result={v}");
|
||||
return Some(FALLBACK_LOCALE.to_owned());
|
||||
}
|
||||
log::warn!("setlocale failed even with the fallback, uncertain results");
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::OsString;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Mock {
|
||||
lang: Option<OsString>,
|
||||
/// Locales `setlocale` accepts, with the "current LANG" rule:
|
||||
/// `""` resolves through `lang`.
|
||||
supported: Vec<&'static str>,
|
||||
c_fallback_when_no_lang: bool,
|
||||
calls: Vec<String>,
|
||||
}
|
||||
|
||||
impl Sys for Mock {
|
||||
fn lang(&self) -> Option<OsString> {
|
||||
self.lang.clone()
|
||||
}
|
||||
fn set_lang(&mut self, value: &str) {
|
||||
self.calls.push(format!("set_lang({value})"));
|
||||
self.lang = Some(value.into());
|
||||
}
|
||||
fn unset_lang(&mut self) {
|
||||
self.calls.push("unset_lang".into());
|
||||
self.lang = None;
|
||||
}
|
||||
fn setlocale(&mut self, locale: &str) -> Option<String> {
|
||||
self.calls.push(format!("setlocale({locale})"));
|
||||
if locale.is_empty() {
|
||||
return match &self.lang {
|
||||
Some(l) if !l.is_empty() => {
|
||||
let l = l.to_string_lossy().into_owned();
|
||||
self.supported.contains(&l.as_str()).then_some(l)
|
||||
}
|
||||
_ => self.c_fallback_when_no_lang.then(|| "C".to_owned()),
|
||||
};
|
||||
}
|
||||
self.supported.contains(&locale).then(|| locale.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn mock(lang: Option<&str>, supported: &[&'static str], c: bool) -> Mock {
|
||||
Mock {
|
||||
lang: lang.map(OsString::from),
|
||||
supported: supported.to_vec(),
|
||||
c_fallback_when_no_lang: c,
|
||||
calls: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_env_locale_is_used() {
|
||||
let mut m = mock(Some("de_DE.UTF-8"), &["de_DE.UTF-8"], false);
|
||||
assert_eq!(ensure_locale_with(&mut m), None);
|
||||
assert_eq!(m.calls, ["setlocale()"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_lang_retries_without_lang() {
|
||||
// Invalid LANG, system default works (not "C").
|
||||
let mut m = mock(Some("bogus"), &[], false);
|
||||
m.supported = vec![];
|
||||
// After unsetting, "" resolves to a real default.
|
||||
struct Sys2(Mock);
|
||||
impl Sys for Sys2 {
|
||||
fn lang(&self) -> Option<OsString> {
|
||||
self.0.lang()
|
||||
}
|
||||
fn set_lang(&mut self, v: &str) {
|
||||
self.0.set_lang(v);
|
||||
}
|
||||
fn unset_lang(&mut self) {
|
||||
self.0.unset_lang();
|
||||
}
|
||||
fn setlocale(&mut self, l: &str) -> Option<String> {
|
||||
self.0.calls.push(format!("setlocale({l})"));
|
||||
(l.is_empty() && self.0.lang.is_none()).then(|| "fr_FR.UTF-8".to_owned())
|
||||
}
|
||||
}
|
||||
let mut s = Sys2(m);
|
||||
assert_eq!(ensure_locale_with(&mut s), None);
|
||||
assert_eq!(
|
||||
s.0.calls,
|
||||
["setlocale()", "set_lang()", "unset_lang", "setlocale()"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_fallback_moves_on_to_en_us_and_exports_lang() {
|
||||
let mut m = mock(Some("bogus"), &["en_US.UTF-8"], true);
|
||||
assert_eq!(ensure_locale_with(&mut m).as_deref(), Some("en_US.UTF-8"));
|
||||
assert_eq!(m.lang.as_deref(), Some(std::ffi::OsStr::new("en_US.UTF-8")));
|
||||
assert_eq!(m.calls.last().unwrap(), "set_lang(en_US.UTF-8)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_works_leaves_lang_unset() {
|
||||
let mut m = mock(Some("bogus"), &[], false);
|
||||
assert_eq!(ensure_locale_with(&mut m), None);
|
||||
assert_eq!(m.lang, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_lang_goes_straight_to_fallback() {
|
||||
let mut m = mock(None, &["en_US.UTF-8"], false);
|
||||
assert_eq!(ensure_locale_with(&mut m).as_deref(), Some("en_US.UTF-8"));
|
||||
// No LANG to unset: setlocale("") then the fallback.
|
||||
assert_eq!(m.calls[0], "setlocale()");
|
||||
assert_eq!(m.calls[1], "setlocale(en_US.UTF-8)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -202,10 +330,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[test]
|
||||
fn ensure_locale_is_noop() {
|
||||
assert_eq!(ensure_locale(), None);
|
||||
fn fallback_locale_value() {
|
||||
assert_eq!(FALLBACK_LOCALE, "en_US.UTF-8");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user