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 0f898d3e41
commit 8f3e206d9f
92 files changed
+2967 -992

No files matched your search

+29 -27
View File
@@ -353,8 +353,8 @@ enum Loaded {
/// A pending side effect requested by the state (needs the terminal).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Effect {
/// Copy the text to the clipboard (OSC 52).
Copy(String),
/// Copy the (decoded) bytes to the clipboard (OSC 52).
Copy(ghostty_os::osbytes::EncodedOs),
}
/// The preview state (Zig: `Preview`).
@@ -373,9 +373,9 @@ pub struct Preview {
mouse: Option<MouseEvent>,
/// The list height as of the last draw (for clamping the window).
list_height: u16,
loaded: Option<(String, Loaded)>,
loaded: Option<(ghostty_os::osbytes::EncodedOs, Loaded)>,
/// Where `w` writes the auto theme file (overridable for tests).
pub auto_theme_path: Option<String>,
pub auto_theme_path: Option<std::path::PathBuf>,
effects: Vec<Effect>,
overlays: Vec<Overlay>,
}
@@ -581,7 +581,8 @@ impl Preview {
}
if is_char('c') {
if let Some(t) = self.selected() {
let name = t.theme.clone();
// Theme names are in the reversible OS-string encoding.
let name = ghostty_os::osbytes::EncodedOs::from_encoded(t.theme.clone());
self.effects.push(Effect::Copy(name));
}
} else if code == KeyCode::Char('C')
@@ -965,10 +966,10 @@ impl Preview {
(child.area.width / 2).saturating_sub(gw(&text) / 2),
);
child.print_ex(
&theme.path,
theme.path.as_encoded(),
ui_err,
middle,
(child.area.width / 2).saturating_sub(gw(&theme.path) / 2),
(child.area.width / 2).saturating_sub(gw(theme.path.as_encoded()) / 2),
Some(&theme.to_uri()),
None,
);
@@ -1024,10 +1025,10 @@ impl Preview {
None,
);
child.print_ex(
&theme.path,
theme.path.as_encoded(),
standard,
2,
(child.area.width / 2).saturating_sub(gw(&theme.path) / 2),
(child.area.width / 2).saturating_sub(gw(theme.path.as_encoded()) / 2),
Some(&uri),
None,
);
@@ -1405,25 +1406,25 @@ fn error_name(e: &ghostty_config::config::Error) -> String {
}
/// `resolveAutoThemePath`: `<config dir>/auto/theme.ghostty`.
pub fn resolve_auto_theme_path() -> Result<String, Error> {
pub fn resolve_auto_theme_path() -> Result<std::path::PathBuf, Error> {
let main_cfg_path = ghostty_config::file_load::preferred_default_file_path()
.map_err(|e| Error::Other(e.to_string()))?;
let main_cfg_path = ghostty_os::osbytes::config_str_to_path(&main_cfg_path);
.map_err(|e| Error::Other(e.to_string()))?
.to_path();
let base_dir = main_cfg_path
.parent()
.ok_or_else(|| Error::Other("BadPathName".to_string()))?;
Ok(ghostty_os::osbytes::path_to_config_string(
&base_dir.join("auto").join("theme.ghostty"),
))
Ok(base_dir.join("auto").join("theme.ghostty"))
}
/// `writeAutoThemeFile`.
pub fn write_auto_theme_file(path: Option<&str>, theme_name: &str) -> Result<(), Error> {
pub fn write_auto_theme_file(
path: Option<&std::path::Path>,
theme_name: &str,
) -> Result<(), Error> {
let auto_path = match path {
Some(p) => p.to_string(),
Some(p) => p.to_path_buf(),
None => resolve_auto_theme_path()?,
};
let auto_path = ghostty_os::osbytes::config_str_to_path(&auto_path);
if let Some(dir) = auto_path.parent() {
std::fs::create_dir_all(dir)?;
}
@@ -1737,11 +1738,7 @@ fn run_loop(app: &mut Preview) -> Result<(), Error> {
match effect {
Effect::Copy(text) => {
let out = terminal.backend_mut();
write!(
out,
"\x1b]52;c;{}\x1b\\",
base64(&ghostty_os::osbytes::config_str_to_bytes(&text))
)?;
write!(out, "\x1b]52;c;{}\x1b\\", base64(&text.to_bytes()))?;
out.flush()?;
}
}
@@ -1788,7 +1785,7 @@ mod tests {
.unwrap();
ThemeListElement {
location: Location::Resources,
path: path.to_string_lossy().into_owned(),
path: ghostty_os::osbytes::EncodedOs::from_path(&path),
theme: (*name).to_string(),
rank: None,
}
@@ -1939,14 +1936,19 @@ mod tests {
KeyModifiers::SHIFT,
)));
let effects = p.take_effects();
assert_eq!(Effect::Copy("alpha".into()), effects[0]);
assert_eq!(
Effect::Copy(tmp.path().join("alpha").to_string_lossy().into_owned()),
Effect::Copy(ghostty_os::osbytes::EncodedOs::from_encoded("alpha")),
effects[0]
);
assert_eq!(
Effect::Copy(ghostty_os::osbytes::EncodedOs::from_path(
&tmp.path().join("alpha")
)),
effects[1]
);
let auto = tmp.path().join("cfg/auto/theme.ghostty");
p.auto_theme_path = Some(auto.to_string_lossy().into_owned());
p.auto_theme_path = Some(auto.clone());
p.update(&key(KeyCode::Enter));
assert_eq!(Mode::Save, p.mode);
p.update(&ch('w'));