fix: C action buffer lengths, byte-faithful callbacks, runtime finalize

Resolves the 8 gaps in docs/rewrite/notes/review-rv10-codex.md
(final-Z13.md):
- P1 memory safety: the C key-table action (and hovered-link/open-URL
  payloads) could advertise a length larger than the NUL-truncated
  allocation. A counted_bytes helper now passes the full bytes plus a
  trailing NUL with len == byte count, like Zig's backing slices; all
  C pointer+len structs were audited and tested to read exactly len.
- ghostty-bench args follow cli/args.zig (ordered duplicates, empty
  value resets, first bad value errors, parseBool spellings); the C
  benchmark tokenizer uses the ArgIteratorGeneral port.
- OSC 7 derived titles, desktop notifications and OSC 8 hover links
  forward raw bytes to the runtime; GTK converts only for display.
- C diagnostic getter exports original bytes; escaped surrogates report
  Utf8CannotEncodeSurrogateHalf; config float formatters print `nan`.
- Config::finalize_for(AppRuntime): GTK-only finalization (gtk-single-
  instance=detect) and termio's cgroup hooks apply only to the GTK
  runtime, so libghostty keeps `detect` like Zig.

Test Plan:
- cargo test --workspace --no-fail-fast (exit 0)
- cargo test -p ghostty-lib --features test-platform
- just check-wasm (macOS/Windows clippy re-run after the concurrent
  terminal changes land)
This commit is contained in:
ddidderr committed 2026-10-12 13:09:04 +02:00
1 parent 7b3fffded3
commit da9599140b
21 files changed
+479 -133

No files matched your search

+30 -35
View File
@@ -5,42 +5,16 @@ use std::ffi::{CStr, c_char};
use ghostty_bench::{args::Args, benches};
/// Split the argument string like `std.process.ArgIteratorGeneral(.{})`:
/// whitespace separated, with double quotes grouping and backslash
/// escapes inside quotes. Works on raw bytes: file names need not be UTF-8.
/// Split the argument string like `std.process.ArgIteratorGeneral(.{})`
/// (backslashes are only special before a double quote). Works on raw bytes:
/// file names need not be UTF-8.
fn split_args(s: &[u8]) -> Vec<std::ffi::OsString> {
let mut out = Vec::new();
let mut cur: Vec<u8> = Vec::new();
let mut have = false;
let mut in_quote = false;
let mut bytes = s.iter().copied();
while let Some(c) = bytes.next() {
match c {
b'"' => {
in_quote = !in_quote;
have = true;
}
b'\\' if in_quote => {
if let Some(n) = bytes.next() {
cur.push(n);
}
}
c if c.is_ascii_whitespace() && !in_quote => {
if have {
out.push(ghostty_os::osbytes::os_from_bytes(std::mem::take(&mut cur)));
have = false;
}
}
c => {
cur.push(c);
have = true;
}
}
}
if have {
out.push(ghostty_os::osbytes::os_from_bytes(cur));
}
out
ghostty_os::arg_iterator::ArgIteratorGeneral::new(
ghostty_os::arg_iterator::Options::default(),
s,
)
.map(ghostty_os::osbytes::os_from_bytes)
.collect()
}
/// Run the Ghostty benchmark CLI with the given action and arguments.
@@ -90,6 +64,27 @@ mod tests {
assert_eq!(split_args(b" ").len(), 0);
}
#[test]
fn split_args_backslashes_are_literal_except_before_quotes() {
assert_eq!(
split_args(br#"--terminal-rows="1\0""#),
[r"--terminal-rows=1\0"]
);
assert_eq!(split_args(b"a\\ b"), [r"a\", "b"]);
assert_eq!(split_args(br#""a\"b""#), ["a\"b"]);
}
#[test]
fn backslash_zero_row_count_fails_like_zig() {
// SAFETY: valid NUL terminated strings.
assert!(!unsafe {
ghostty_benchmark_cli(
c"terminal-stream".as_ptr(),
c"--terminal-rows=\"1\\0\"".as_ptr(),
)
});
}
#[cfg(unix)]
#[test]
fn split_args_keeps_non_utf8_bytes() {