fix: systematic byte fidelity for config strings and RV7 gaps

Resolves the 9 gap groups in docs/rewrite/notes/review-rv7-codex.md and
audits every string-typed config field against its consumers
(field -> consumers -> decode status table in final-Z9.md):
- Windows EnvMap keys use a port of Zig's nls.zig uppercase table
  (upcaseW); Windows locale init follows Zig's chain (setlocale +
  _putenv_s); malformed WTF-8 now fails process launch conversions like
  Zig instead of substituting U+FFFD; kitty file paths convert WTF-8.
- bash --rcfile/--init-file, configured TERM and inherited GTK LANG
  keep their original bytes.
- C API clipboard completion and ghostty_surface_binding_action take
  bytes; complete_clipboard_request is byte-typed through the core.
- text:/csi:/esc: bindings, enquiry-response, input = path:, embedded
  initial_input, search needles, titles, link regexes, clipboard
  codepoint map, command palette text and GTK font/namespace values
  decode the escape encoding before use.

Remaining by design: D-Bus a{ss} values and +new-window arguments are
UTF-8 by D-Bus type. C API key/preedit text follows separately.

Test Plan:
- cargo test --workspace
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 10:50:20 +02:00
1 parent ffc91c31e4
commit af094276f8
40 files changed
+986 -167

No files matched your search

+35 -11
View File
@@ -79,6 +79,10 @@ pub enum Error {
Start(#[from] command::StartError),
#[error(transparent)]
ShellIntegration(#[from] shell_integration::Error),
/// A configured path, argument or environment value is not well-formed
/// WTF-8 (Zig's process launch conversions fail on such input).
#[error(transparent)]
InvalidWtf8(#[from] ghostty_os::osbytes::InvalidWtf8),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("failed to get WINDIR, cannot run shell command")]
@@ -102,12 +106,12 @@ pub struct Config {
pub cursor_blink: Option<bool>,
pub working_directory: Option<EncodedOs>,
pub resources_dir: Option<EncodedOs>,
pub term: String,
pub term: EncodedOs,
}
impl Config {
/// A config with Zig-struct-style defaults for everything optional.
pub fn new(env: EnvMap, term: impl Into<String>) -> Config {
pub fn new(env: EnvMap, term: EncodedOs) -> Config {
Config {
command: None,
env,
@@ -117,7 +121,7 @@ impl Config {
cursor_blink: None,
working_directory: None,
resources_dir: None,
term: term.into(),
term,
}
}
}
@@ -585,13 +589,20 @@ fn write_thread_main(handle: &OwnedHandle, rx: &Receiver<Vec<u8>>) {
/// 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) {
///
/// Malformed WTF-8 fails (Zig's conversions for the environment block are
/// fallible) instead of being substituted with U+FFFD.
fn apply_env_overrides(
env: &mut EnvMap,
overrides: &RepeatableStringMap,
) -> Result<(), ghostty_os::osbytes::InvalidWtf8> {
for (k, v) in overrides.iter() {
env.insert(
ghostty_os::osbytes::config_str_to_os(k),
ghostty_os::osbytes::config_str_to_os(v),
EncodedOs::from_encoded(k).try_to_os()?,
EncodedOs::from_encoded(v).try_to_os()?,
);
}
Ok(())
}
struct Subprocess {
@@ -617,18 +628,20 @@ impl Subprocess {
log::info!("found Ghostty resources dir: {}", dir.as_encoded());
// The resources dir is an encoded OS string: decode it for the
// child's environment.
env.insert("GHOSTTY_RESOURCES_DIR", dir.to_os());
env.insert("GHOSTTY_RESOURCES_DIR", dir.try_to_os()?);
}
// Set our TERM var. This is a bit complicated because we want to use
// the ghostty TERM value but we want to only do that if we have
// ghostty in the TERMINFO database.
if let Some(base) = &cfg.resources_dir {
env.insert("TERM", cfg.term.clone());
// The configured TERM is an encoded OS string: export its bytes.
env.insert("TERM", cfg.term.try_to_os()?);
env.insert("COLORTERM", "truecolor");
// Assume that the resources directory is adjacent to the terminfo
// database
base.try_to_os()?; // malformed WTF-8 fails like Zig
env.insert("TERMINFO", terminfo_dir(base));
} else {
env.insert("TERM", "xterm-256color");
@@ -698,7 +711,7 @@ impl Subprocess {
};
// Add the environment variables that override any others.
apply_env_overrides(&mut env, &cfg.env_override);
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.
@@ -720,7 +733,7 @@ impl Subprocess {
// Propagate the current working directory (CWD) to the shell.
if let Some(pwd) = &cwd {
env.insert("PWD", pwd.to_os());
env.insert("PWD", pwd.try_to_os()?);
}
Ok(Subprocess {
@@ -948,7 +961,7 @@ mod tests {
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);
super::apply_env_overrides(&mut env, &m).unwrap();
assert_eq!(
env.get("A").unwrap(),
std::ffi::OsString::from_wide(&[0x61, 0xD800])
@@ -956,6 +969,17 @@ mod tests {
assert_eq!(env.get("K\u{fdd0}").unwrap(), "v");
}
#[test]
fn malformed_wtf8_env_overrides_fail() {
use ghostty_config::cli_args::ConfigField;
let bad = EncodedOs::from_bytes(b"a\xff").into_encoded();
let mut m = RepeatableStringMap::default();
m.parse_cli(Some(&format!("A={bad}"))).unwrap();
let mut env = EnvMap::new();
assert!(super::apply_env_overrides(&mut env, &m).is_err());
}
#[test]
fn terminfo_dir_is_sibling_of_resources_and_lossless() {
use std::os::windows::ffi::{OsStrExt, OsStringExt};