refactor: type escaped OS strings and fix the final audit findings

Escaped config strings (ghostty_os::osbytes) kept reaching OS
boundaries undecoded, and decoding was occasionally applied to plain
text. The root cause was that both were plain `String`s. Now:
- `EncodedOs` is a newtype (no Display/AsRef<Path>) that decodes only
  via to_os/to_path/to_bytes/to_wide; plain text enters via
  from_plain_text. OS helpers (env, home, xdg, passwd, resources dir,
  path expansion), config::Path, WorkingDirectory, theme/file_load/edit
  paths, terminal pwd, OpenUrl/Pwd actions and termio cwd use it, so the
  compiler forces decoding at every boundary. os::open takes &OsStr.
  Config output is unchanged.

Fixes all rows of docs/rewrite/notes/review-rv5-codex.md (final-Z7.md):
screen-file copy; regex/OSC 8 URLs no longer decoded (regression);
lossless Windows WTF-8/WTF-16 for args, env and CreateProcessW; Windows
resources dir/TERMINFO; color OSC request lists and X11 color lookup no
longer allocate globally (C zero-allocation test extended); macOS Mach
handler resumes all threads before KERN_FAILURE (hang regression) and
SIGABRT minidumps go through the exception thread; plus open-config,
background images, crash dir, ssh cache, exe dir/PATH, fontconfig
filenames, file links, kitty TMPDIR, .hushlogin and benchmark paths.
The typing also surfaced and fixed gtk-custom-css, bell audio, bash
HISTFILE, OSC 52 writes and the C config getters.

Test Plan:
- cargo test --workspace (3376 passed)
- just check-macos (both), just check-windows, just check-wasm
- diff <(ghostty-rs +show-config --default --docs) \
  <(ghostty +show-config --default --docs)
This commit is contained in:
ddidderr committed 2026-10-12 10:06:07 +02:00
1 parent 7a3f0b0f2c
commit f7fec6f44d
92 files changed
+2967 -992

No files matched your search

+52 -37
View File
@@ -29,7 +29,7 @@ const MAX_PATH_BYTES: usize = 4096;
///
/// Non-UTF-8 bytes are preserved through [`crate::osbytes`]; use
/// [`home_os`] for the raw OS string.
pub fn home() -> Result<Option<String>, Error> {
pub fn home() -> Result<Option<osbytes::EncodedOs>, Error> {
home_with_capacity(MAX_PATH_BYTES)
}
@@ -40,8 +40,8 @@ pub fn home_os() -> Result<Option<OsString>, Error> {
/// Like [`home`] but emulates the fixed output buffer of the Zig version:
/// errors with `BufferTooSmall` if the result is longer than `cap` bytes.
pub fn home_with_capacity(cap: usize) -> Result<Option<String>, Error> {
Ok(home_os_with_capacity(cap)?.map(|h| osbytes::os_to_config_string(&h)))
pub fn home_with_capacity(cap: usize) -> Result<Option<osbytes::EncodedOs>, Error> {
Ok(home_os_with_capacity(cap)?.map(|h| osbytes::EncodedOs::from_os(&h)))
}
/// Byte-preserving [`home_with_capacity`].
@@ -98,25 +98,29 @@ pub fn home_os_with_capacity(cap: usize) -> Result<Option<OsString>, Error> {
/// either variable is missing.
#[cfg(windows)]
pub fn home_os_with_capacity(cap: usize) -> Result<Option<OsString>, Error> {
let drive = std::env::var_os("HOMEDRIVE").map(|v| v.to_string_lossy().into_owned());
let path = std::env::var_os("HOMEPATH").map(|v| v.to_string_lossy().into_owned());
Ok(home_windows(drive.as_deref(), path.as_deref(), cap)?.map(OsString::from))
let drive = std::env::var_os("HOMEDRIVE");
let path = std::env::var_os("HOMEPATH");
home_windows(drive.as_deref(), path.as_deref(), cap)
}
/// Pure part of [`home_with_capacity`] on Windows.
#[cfg(any(windows, test))]
fn home_windows(
drive: Option<&str>,
path: Option<&str>,
drive: Option<&OsStr>,
path: Option<&OsStr>,
cap: usize,
) -> Result<Option<String>, Error> {
) -> Result<Option<OsString>, Error> {
let (Some(drive), Some(path)) = (drive, path) else {
return Ok(None);
};
if cap < drive.len() + path.len() {
// Lossless (WTF-16 safe) concatenation; the size is measured in WTF-8
// bytes like the Zig buffer.
if cap < osbytes::os_bytes(drive).len() + osbytes::os_bytes(path).len() {
return Err(Error::BufferTooSmall);
}
Ok(Some(format!("{drive}{path}")))
let mut home = drive.to_owned();
home.push(path);
Ok(Some(home))
}
#[cfg(unix)]
@@ -135,7 +139,7 @@ fn trim_space(input: &[u8]) -> &[u8] {
///
/// Errors if `home` fails or if the size of the expanded path is larger
/// than the maximum path length.
pub fn expand_home(path: &str) -> Result<String, ExpandError> {
pub fn expand_home(path: &osbytes::EncodedOs) -> Result<osbytes::EncodedOs, ExpandError> {
expand_home_with_capacity(path, MAX_PATH_BYTES)
}
@@ -146,9 +150,11 @@ pub fn expand_home_os(path: &OsStr) -> Result<OsString, ExpandError> {
/// Like [`expand_home`] with an explicit output capacity (emulating the
/// caller-provided buffer of the Zig version).
pub fn expand_home_with_capacity(path: &str, cap: usize) -> Result<String, ExpandError> {
expand_home_os_with_capacity(&osbytes::config_str_to_os(path), cap)
.map(|p| osbytes::os_to_config_string(&p))
pub fn expand_home_with_capacity(
path: &osbytes::EncodedOs,
cap: usize,
) -> Result<osbytes::EncodedOs, ExpandError> {
expand_home_os_with_capacity(&path.to_os(), cap).map(osbytes::EncodedOs::from)
}
/// Windows: `~/` is not an idiom generally used on Windows.
@@ -183,13 +189,17 @@ mod tests {
#[test]
fn home_windows_joins_drive_and_path() {
assert_eq!(
home_windows(Some("C:"), Some("\\Users\\me"), 4096),
Ok(Some("C:\\Users\\me".to_owned()))
home_windows(
Some(OsStr::new("C:")),
Some(OsStr::new("\\Users\\me")),
4096
),
Ok(Some(OsString::from("C:\\Users\\me")))
);
assert_eq!(home_windows(None, Some("\\x"), 4096), Ok(None));
assert_eq!(home_windows(Some("C:"), None, 4096), Ok(None));
assert_eq!(home_windows(None, Some(OsStr::new("\\x")), 4096), Ok(None));
assert_eq!(home_windows(Some(OsStr::new("C:")), None, 4096), Ok(None));
assert_eq!(
home_windows(Some("C:"), Some("\\Users"), 3),
home_windows(Some(OsStr::new("C:")), Some(OsStr::new("\\Users")), 3),
Err(Error::BufferTooSmall)
);
}
@@ -204,28 +214,30 @@ mod tests_unix {
fn expand_home_unix() {
with_env_lock(|| {
let cap = MAX_PATH_BYTES;
let home_dir = expand_home_with_capacity("~/", cap).unwrap();
let ex =
|s: &str| expand_home_with_capacity(&osbytes::EncodedOs::from_plain_text(s), cap);
let home_dir = ex("~/").unwrap();
// Joining the home directory `~` with the path `/`
// the result should end with a separator here. (e.g. `/home/user/`)
assert!(home_dir.ends_with('/'));
assert!(home_dir.ends_with_plain("/"));
let downloads = expand_home_with_capacity("~/Downloads/shader.glsl", cap).unwrap();
let expected_downloads = format!("{home_dir}Downloads/shader.glsl");
assert_eq!(expected_downloads, downloads);
let downloads = ex("~/Downloads/shader.glsl").unwrap();
let expected_downloads = format!("{}Downloads/shader.glsl", home_dir.as_encoded());
assert_eq!(downloads, expected_downloads.as_str());
assert_eq!("~", expand_home_with_capacity("~", cap).unwrap());
assert_eq!("~abc/", expand_home_with_capacity("~abc/", cap).unwrap());
assert_eq!(
"/home/user",
expand_home_with_capacity("/home/user", cap).unwrap()
);
assert_eq!("", expand_home_with_capacity("", cap).unwrap());
assert_eq!(ex("~").unwrap(), "~");
assert_eq!(ex("~abc/").unwrap(), "~abc/");
assert_eq!(ex("/home/user").unwrap(), "/home/user");
assert_eq!(ex("").unwrap(), "");
// Expect an error if the buffer is large enough to hold the home directory,
// but not the expanded path
assert_eq!(
Err(ExpandError::BufferTooSmall),
expand_home_with_capacity("~/Downloads", home_dir.len())
expand_home_with_capacity(
&osbytes::EncodedOs::from_plain_text("~/Downloads"),
home_dir.as_encoded().len()
)
);
});
}
@@ -248,16 +260,19 @@ mod tests_unix {
let e = expand_home_os(OsStr::from_bytes(b"~/x\xfe")).unwrap();
assert_eq!(e.as_bytes(), b"/home/\xffu/x\xfe");
// The String API round-trips through the escape encoding.
let s = expand_home("~/y").unwrap();
assert_eq!(osbytes::config_str_to_bytes(&s), b"/home/\xffu/y");
let s = expand_home(&osbytes::EncodedOs::from_plain_text("~/y")).unwrap();
assert_eq!(s.to_bytes(), b"/home/\xffu/y");
});
}
#[test]
fn home_prefers_env() {
temp_env::with_var("HOME", Some("/some/home"), || {
assert_eq!(home().unwrap().as_deref(), Some("/some/home"));
assert_eq!(expand_home("~/x").unwrap(), "/some/home/x");
assert_eq!(home().unwrap().unwrap(), "/some/home");
assert_eq!(
expand_home(&osbytes::EncodedOs::from_plain_text("~/x")).unwrap(),
"/some/home/x"
);
assert_eq!(home_with_capacity(3), Err(Error::BufferTooSmall));
});
}