refactor(config): parse config values from raw bytes like Zig

Zig parses config values (cli/args.zig, Config.zig field parsers,
config/string.zig) from raw byte slices, and each field type decides
whether it requires UTF-8 (InvalidValue otherwise) or stores bytes. The
port escape-encoded raw bytes into Strings at the line reader and let
fields parse the escaped text, so fields could observe encoding
artifacts; successive audits kept finding such leaks (RV5-RV8).

Field parsers now receive the original bytes: text fields validate UTF-8
where Zig does, byte/path fields store bytes (EncodedOs/OsString). This
resolves all 9 RV8 findings (docs/rewrite/notes/review-rv8-codex.md),
each with a regression test: Windows path conversion fails with
BadPathName instead of substituting U+FFFD; Windows process launch uses
strict UTF-8 like Zig's Command; the WTF-8 decoder accepts paired-
surrogate encodings like Zig; literal U+FDD0 Unicode key triggers;
selection-word-chars and clipboard-codepoint-map reject invalid UTF-8;
font style / codepoint-map family names use original bytes; C title
actions forward bytes; D-Bus new-window/Flatpak conversions match Zig.
Design, sweep and validation: docs/rewrite/notes/final-bytes-refactor.md.

Also moves the bundled iTerm2-Color-Schemes license out of
resources/themes/ so +list-themes no longer lists it as a theme
(output is now byte-identical to Ghostty 1.3.1).

Done by Codex (gpt-6-astra), reviewed and verified by the orchestrator.

Test Plan:
- cargo test --workspace --no-fail-fast (3428 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
- +show-config --default --docs, +list-keybinds, +list-actions --docs,
  +list-themes --plain compared with /usr/bin/ghostty
This commit is contained in:
ddidderr committed 2026-10-12 11:57:01 +02:00
1 parent 70c250cf2d
commit 6f9b3129e1
100 files changed
+2310 -1054

No files matched your search

+29 -2
View File
@@ -45,6 +45,7 @@ struct Rec {
/// (action tag, target tag, surface pointer)
actions: Vec<(i32, i32, usize)>,
titles: Vec<String>,
raw_titles: Vec<Vec<u8>>,
cell_size: Option<(u32, u32)>,
writes: Vec<(i32, String, bool)>,
reads: Vec<(i32, usize)>,
@@ -86,6 +87,7 @@ unsafe extern "C" fn on_action(
// set_title
let t = unsafe { CStr::from_ptr(action.action.set_title.title) };
r.titles.push(t.to_string_lossy().into_owned());
r.raw_titles.push(t.to_bytes().to_vec());
}
25 => {
let c = unsafe { action.action.cell_size };
@@ -167,7 +169,7 @@ fn cs(s: &str) -> CString {
}
impl Host {
fn new(extra_config: &str) -> Option<Host> {
fn new(extra_config: impl AsRef<[u8]>) -> Option<Host> {
rec(|r| *r = Rec::default());
let gl = match Headless::new() {
Ok(g) => Rc::new(g),
@@ -186,9 +188,11 @@ impl Host {
write!(
conf,
"shell-integration = none\nwindow-padding-x = 0\nwindow-padding-y = 0\n\
font-size = 12\nscrollback-limit = 100000\n{extra_config}\n"
font-size = 12\nscrollback-limit = 100000\n"
)
.unwrap();
conf.write_all(extra_config.as_ref()).unwrap();
conf.write_all(b"\n").unwrap();
let cfg = ghostty_config_new();
let path = cs(conf.path().to_str().unwrap());
unsafe {
@@ -856,3 +860,26 @@ fn selection_clipboard_support_is_reported() {
});
unsafe { ghostty_surface_free(s) };
}
#[test]
fn configured_and_binding_titles_reach_c_as_original_bytes() {
let Some(h) = Host::new(b"title=config\xff\xef\xb7\x90\xc3\xa9") else {
return;
};
let surface = h.surface("sleep 30");
assert!(rec(|r| r
.raw_titles
.iter()
.any(|v| v == b"config\xff\xef\xb7\x90\xc3\xa9")));
let action = b"set_surface_title:binding\xfe\xef\xb7\x90\xc3\xa9";
// SAFETY: a live surface and the action slice is valid for the entire call.
assert!(unsafe {
ghostty_surface_binding_action(surface, action.as_ptr().cast(), action.len())
});
assert_eq!(
rec(|r| r.raw_titles.last().cloned()).unwrap(),
b"binding\xfe\xef\xb7\x90\xc3\xa9"
);
// SAFETY: the surface is live and owned by this test.
unsafe { ghostty_surface_free(surface) };
}