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:
1 parent
0f898d3e41
commit
8f3e206d9f
92 files changed
+2967
-992
No files matched your search
@@ -1,21 +1,88 @@
|
||||
//! Kitty color protocol (ZIG/src/terminal/kitty/color.zig).
|
||||
|
||||
use std::fmt;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use crate::{color::Rgb, osc::Terminator};
|
||||
|
||||
/// OSC 21 payload.
|
||||
///
|
||||
/// The request list is stored encoded (fixed size records) in the OSC
|
||||
/// parser's own storage, so parsing it never touches the global allocator
|
||||
/// when the parser was given a caller-provided allocator (C API).
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct OSC {
|
||||
pub struct OSC<'a> {
|
||||
/// list of requests
|
||||
pub list: Vec<Request>,
|
||||
pub list: List<'a>,
|
||||
|
||||
/// We must reply with the same string terminator (ST) as used in the
|
||||
/// request.
|
||||
pub terminator: Terminator,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
impl OSC<'_> {
|
||||
/// Detach from the parser storage.
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> OSC<'static> {
|
||||
OSC {
|
||||
list: self.list.into_owned(),
|
||||
terminator: self.terminator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of one encoded [`Request`] record.
|
||||
pub const RECORD_LEN: usize = 8;
|
||||
|
||||
/// A list of kitty color [`Request`]s: borrowed (or owned) encoded records.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct List<'a>(Cow<'a, [u8]>);
|
||||
|
||||
impl<'a> List<'a> {
|
||||
/// View records encoded with [`Request::encode`] (`bytes.len()` must be a
|
||||
/// multiple of [`RECORD_LEN`]).
|
||||
#[must_use]
|
||||
pub fn from_encoded(bytes: &'a [u8]) -> List<'a> {
|
||||
debug_assert_eq!(bytes.len() % RECORD_LEN, 0);
|
||||
List(Cow::Borrowed(bytes))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len() / RECORD_LEN
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, i: usize) -> Option<Request> {
|
||||
Request::decode(self.0.get(i * RECORD_LEN..(i + 1) * RECORD_LEN)?)
|
||||
}
|
||||
|
||||
/// Iterate the requests (by value).
|
||||
pub fn iter(&self) -> impl Iterator<Item = Request> + '_ {
|
||||
self.0
|
||||
.as_chunks::<RECORD_LEN>()
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|rec| Request::decode(rec))
|
||||
}
|
||||
|
||||
/// Copy into a `Vec` (allocates; for tests and diagnostics).
|
||||
#[must_use]
|
||||
pub fn to_vec(&self) -> Vec<Request> {
|
||||
self.iter().collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_owned(self) -> List<'static> {
|
||||
List(Cow::Owned(self.0.into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Request {
|
||||
Query(Kind),
|
||||
Set { key: Kind, color: Rgb },
|
||||
@@ -35,6 +102,26 @@ pub enum Special {
|
||||
}
|
||||
|
||||
impl Special {
|
||||
/// All specials in discriminant order.
|
||||
const ALL: [Special; 8] = [
|
||||
Special::Foreground,
|
||||
Special::Background,
|
||||
Special::SelectionForeground,
|
||||
Special::SelectionBackground,
|
||||
Special::Cursor,
|
||||
Special::CursorText,
|
||||
Special::VisualBell,
|
||||
Special::SecondTransparentBackground,
|
||||
];
|
||||
|
||||
fn to_u8(self) -> u8 {
|
||||
Special::ALL.iter().position(|s| *s == self).unwrap_or(0) as u8
|
||||
}
|
||||
|
||||
fn from_u8(v: u8) -> Option<Special> {
|
||||
Special::ALL.get(usize::from(v)).copied()
|
||||
}
|
||||
|
||||
/// The tag name (Zig `@tagName`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
@@ -92,6 +179,46 @@ impl Kind {
|
||||
}
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Encode as a fixed size record: `[tag, kind class, kind value, r, g, b,
|
||||
/// 0, 0]`.
|
||||
#[must_use]
|
||||
pub fn encode(&self) -> [u8; RECORD_LEN] {
|
||||
let (tag, key, color) = match *self {
|
||||
Request::Query(k) => (0, k, Rgb::default()),
|
||||
Request::Set { key, color } => (1, key, color),
|
||||
Request::Reset(k) => (2, k, Rgb::default()),
|
||||
};
|
||||
let (class, value) = match key {
|
||||
Kind::Palette(p) => (0, p),
|
||||
Kind::Special(s) => (1, s.to_u8()),
|
||||
};
|
||||
[tag, class, value, color.r, color.g, color.b, 0, 0]
|
||||
}
|
||||
|
||||
/// Inverse of [`Request::encode`].
|
||||
#[must_use]
|
||||
pub fn decode(rec: &[u8]) -> Option<Request> {
|
||||
let [tag, class, value, r, g, b, ..] = *rec else {
|
||||
return None;
|
||||
};
|
||||
let key = match class {
|
||||
0 => Kind::Palette(value),
|
||||
1 => Kind::Special(Special::from_u8(value)?),
|
||||
_ => return None,
|
||||
};
|
||||
Some(match tag {
|
||||
0 => Request::Query(key),
|
||||
1 => Request::Set {
|
||||
key,
|
||||
color: Rgb { r, g, b },
|
||||
},
|
||||
2 => Request::Reset(key),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Kind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -106,6 +233,33 @@ mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn request_records_round_trip() {
|
||||
let mut reqs = vec![];
|
||||
for s in Special::ALL {
|
||||
let k = Kind::Special(s);
|
||||
reqs.push(Request::Query(k));
|
||||
reqs.push(Request::Reset(k));
|
||||
reqs.push(Request::Set {
|
||||
key: k,
|
||||
color: Rgb { r: 1, g: 2, b: 3 },
|
||||
});
|
||||
}
|
||||
reqs.push(Request::Set {
|
||||
key: Kind::Palette(255),
|
||||
color: Rgb { r: 255, g: 0, b: 9 },
|
||||
});
|
||||
reqs.push(Request::Query(Kind::Palette(0)));
|
||||
let bytes: Vec<u8> = reqs.iter().flat_map(Request::encode).collect();
|
||||
let list = List::from_encoded(&bytes);
|
||||
assert_eq!(list.len(), reqs.len());
|
||||
assert_eq!(list.to_vec(), reqs);
|
||||
assert_eq!(list.get(reqs.len()), None);
|
||||
assert_eq!(list.into_owned().to_vec(), reqs);
|
||||
assert_eq!(Request::decode(&[9, 0, 0, 0, 0, 0, 0, 0]), None);
|
||||
assert_eq!(Request::decode(&[0, 1, 200, 0, 0, 0, 0, 0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_kitty_color_protocol_kind_string() {
|
||||
assert_eq!("foreground", Kind::Special(Special::Foreground).to_string());
|
||||
|
||||
Reference in new issue
Block a user