fix(config): match Zig's numeric grammar, NUL handling and diagnostics
Resolves the 6 differential gap groups in docs/rewrite/notes/review- rv9-codex.md (details in final-Z11.md): - ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std tests and replaces every int/float parse in config, keybind parameters, ssh-cache and desktop detection. - Interior NUL no longer panics in font names and ends C strings like Zig's backing bytes (fontconfig, C config getters, title actions, clipboard callback, relative paths, config-file, shaders, background image, GTK CSS). - GTK new-window arguments are trimmed of ASCII whitespace only. - Command tokenization uses a port of std.process.ArgIteratorGeneral (incl. CR/LF separators) with its std tests. - UTF-8 validation happens where Zig's field parsers do it, so error kinds match (metric modifiers, quick-terminal size, link, ...). A scripted differential run of 202 fields x ~220 hostile/valid values against /usr/bin/ghostty (+validate-config and +show-config: exit code, stdout, stderr) now has zero mismatches besides build-specific defaults (theme resources path, auto-update-channel, gtk-opengl-debug); it also surfaced and fixed background-blur failure state, NaN formatting and NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/ zig_parity.rs. Test Plan: - cargo test --workspace --no-fail-fast (3456 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
1 parent
61e7a680ac
commit
a57ff41d34
26 files changed
+1835
-228
No files matched your search
@@ -177,10 +177,8 @@ impl SplitResizeParameter {
|
||||
pub(super) fn parse_param(param: &str) -> Result<SplitResizeParameter, Error> {
|
||||
let mut it = param.split(',');
|
||||
let dir = SplitResizeDirection::parse_param(it.next().ok_or(Error::InvalidFormat)?)?;
|
||||
let amt = it
|
||||
.next()
|
||||
.ok_or(Error::InvalidFormat)?
|
||||
.parse::<u16>()
|
||||
let amt = it.next().ok_or(Error::InvalidFormat)?;
|
||||
let amt = ghostty_os::zig_fmt::parse_int::<u16>(amt.as_bytes(), 10)
|
||||
.map_err(|_| Error::InvalidFormat)?;
|
||||
// If we have extra parameters it is an error
|
||||
if it.next().is_some() {
|
||||
@@ -304,16 +302,23 @@ pub(super) trait ParseNum: Sized {
|
||||
fn parse_param(s: &str) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
macro_rules! num_param {
|
||||
macro_rules! int_param {
|
||||
($($t:ty),*) => {$(
|
||||
impl ParseNum for $t {
|
||||
fn parse_param(s: &str) -> Result<$t, Error> {
|
||||
s.parse::<$t>().map_err(|_| Error::InvalidFormat)
|
||||
ghostty_os::zig_fmt::parse_int::<$t>(s.as_bytes(), 10)
|
||||
.map_err(|_| Error::InvalidFormat)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
num_param!(f32, usize, isize, i16);
|
||||
int_param!(usize, isize, i16);
|
||||
|
||||
impl ParseNum for f32 {
|
||||
fn parse_param(s: &str) -> Result<f32, Error> {
|
||||
ghostty_os::zig_fmt::parse_float::<f32>(s.as_bytes()).map_err(|_| Error::InvalidFormat)
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape in the style of Zig's `std.zig.stringEscape`.
|
||||
pub(super) fn string_escape(s: &[u8], out: &mut String) {
|
||||
@@ -378,12 +383,14 @@ impl Action {
|
||||
"set_tab_title" => Ok(Self::SetTabTitle(bytes()?)),
|
||||
"activate_key_table" => Ok(Self::ActivateKeyTable(bytes()?)),
|
||||
"activate_key_table_once" => Ok(Self::ActivateKeyTableOnce(bytes()?)),
|
||||
// Zig never validates UTF-8 for enum/numeric parameters: the
|
||||
// bytes just fail the field's own grammar (`InvalidFormat`) for
|
||||
// a known action and `InvalidAction` for an unknown name. U+FFFD
|
||||
// is accepted by none of those grammars, so it stands in for any
|
||||
// invalid parameter.
|
||||
_ => Self::parse_named(
|
||||
name,
|
||||
param
|
||||
.map(std::str::from_utf8)
|
||||
.transpose()
|
||||
.map_err(|_| Error::InvalidAction)?,
|
||||
param.map(|p| std::str::from_utf8(p).unwrap_or("\u{fffd}")),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1930,15 +1937,15 @@ impl Action {
|
||||
}
|
||||
Action::IncreaseFontSize(v) => {
|
||||
out.push(':');
|
||||
out.push_str(&v.to_string());
|
||||
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||
}
|
||||
Action::DecreaseFontSize(v) => {
|
||||
out.push(':');
|
||||
out.push_str(&v.to_string());
|
||||
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||
}
|
||||
Action::SetFontSize(v) => {
|
||||
out.push(':');
|
||||
out.push_str(&v.to_string());
|
||||
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||
}
|
||||
Action::Search(v) => {
|
||||
out.push(':');
|
||||
@@ -1954,7 +1961,7 @@ impl Action {
|
||||
}
|
||||
Action::ScrollPageFractional(v) => {
|
||||
out.push(':');
|
||||
out.push_str(&v.to_string());
|
||||
out.push_str(&ghostty_os::zig_fmt::format_float_d(*v));
|
||||
}
|
||||
Action::ScrollPageLines(v) => {
|
||||
out.push(':');
|
||||
|
||||
@@ -173,11 +173,7 @@ impl<'a> Parser<'a> {
|
||||
// This is kind of hacky but we put a dummy trigger
|
||||
// for chained inputs. The `next` will never yield this
|
||||
// because we have chain set.
|
||||
input: if chain {
|
||||
"a"
|
||||
} else {
|
||||
std::str::from_utf8(&input[..eql_idx]).map_err(|_| Error::InvalidFormat)?
|
||||
},
|
||||
input: if chain { b"a" } else { &input[..eql_idx] },
|
||||
i: 0,
|
||||
},
|
||||
action: Action::parse_bytes(&input[eql_idx + 1..])?,
|
||||
@@ -279,13 +275,16 @@ pub struct SequenceIterator<'a> {
|
||||
/// The input of triggers. This is expected to be ONLY triggers. Things
|
||||
/// like the "unconsumed:" prefix or action must be stripped before
|
||||
/// passing to this iterator.
|
||||
pub input: &'a str,
|
||||
pub input: &'a [u8],
|
||||
pub i: usize,
|
||||
}
|
||||
|
||||
impl<'a> SequenceIterator<'a> {
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
SequenceIterator { input, i: 0 }
|
||||
SequenceIterator {
|
||||
input: input.as_bytes(),
|
||||
i: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next trigger in the sequence if there is no parsing error.
|
||||
@@ -294,9 +293,9 @@ impl<'a> SequenceIterator<'a> {
|
||||
return Ok(None);
|
||||
}
|
||||
let rem = &self.input[self.i..];
|
||||
let idx = rem.find('>').unwrap_or(rem.len());
|
||||
let idx = rem.iter().position(|b| *b == b'>').unwrap_or(rem.len());
|
||||
self.i += idx + 1;
|
||||
Ok(Some(Trigger::parse(&rem[..idx])?))
|
||||
Ok(Some(Trigger::parse_bytes(&rem[..idx])?))
|
||||
}
|
||||
|
||||
/// Returns true if there are no more triggers to parse.
|
||||
@@ -545,19 +544,28 @@ impl Trigger {
|
||||
/// not be part of a sequence (i.e. `a>b`). This parses exactly a single
|
||||
/// trigger.
|
||||
pub fn parse(input: &str) -> Result<Trigger, Error> {
|
||||
Trigger::parse_bytes(input.as_bytes())
|
||||
}
|
||||
|
||||
/// [`Trigger::parse`] over the original bytes. Invalid UTF-8 can only
|
||||
/// reach (and fail) the key lookups, which all end in `InvalidFormat`.
|
||||
pub fn parse_bytes(input: &[u8]) -> Result<Trigger, Error> {
|
||||
if input.is_empty() {
|
||||
return Err(Error::InvalidFormat);
|
||||
}
|
||||
let mut result = Trigger::default();
|
||||
let mut rem: &str = input;
|
||||
let mut rem: &[u8] = input;
|
||||
'outer: while !rem.is_empty() {
|
||||
let idx = rem.find('+').unwrap_or(rem.len());
|
||||
let idx = rem.iter().position(|b| *b == b'+').unwrap_or(rem.len());
|
||||
let part = &rem[..idx];
|
||||
rem = if idx >= rem.len() {
|
||||
""
|
||||
b""
|
||||
} else {
|
||||
&rem[idx + 1..]
|
||||
};
|
||||
let Ok(part) = std::str::from_utf8(part) else {
|
||||
return Err(Error::InvalidFormat);
|
||||
};
|
||||
|
||||
// Check if its a modifier
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user