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
@@ -506,6 +506,42 @@ pub fn parse_packed_struct_with(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Zig's `@max` on floats: a NaN operand yields the other operand and
|
||||
/// `max(+0, -0)` is `+0`.
|
||||
#[must_use]
|
||||
pub fn zig_max(a: f64, b: f64) -> f64 {
|
||||
if a.is_nan() {
|
||||
b
|
||||
} else if b.is_nan() {
|
||||
a
|
||||
} else if a == b {
|
||||
if a.is_sign_negative() { b } else { a }
|
||||
} else {
|
||||
a.max(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig's `@min` on floats: a NaN operand yields the other operand and
|
||||
/// `min(+0, -0)` is `-0`.
|
||||
#[must_use]
|
||||
pub fn zig_min(a: f64, b: f64) -> f64 {
|
||||
if a.is_nan() {
|
||||
b
|
||||
} else if b.is_nan() {
|
||||
a
|
||||
} else if a == b {
|
||||
if a.is_sign_negative() { a } else { b }
|
||||
} else {
|
||||
a.min(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// `std.math.clamp` on floats (`@max(lower, @min(val, upper))`).
|
||||
#[must_use]
|
||||
pub fn zig_clamp(val: f64, lower: f64, upper: f64) -> f64 {
|
||||
zig_max(lower, zig_min(val, upper))
|
||||
}
|
||||
|
||||
pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
||||
match v {
|
||||
"1" | "t" | "T" | "true" => Ok(true),
|
||||
@@ -514,42 +550,6 @@ pub fn parse_bool(v: &str) -> Result<bool, ArgError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an integer like Zig's `std.fmt.parseInt(T, s, 0)`: optional sign,
|
||||
/// optional `0x`/`0o`/`0b` prefix, underscores between digits allowed.
|
||||
pub fn parse_int_base0(s: &str) -> Option<i128> {
|
||||
let (neg, rest) = match s.as_bytes().first()? {
|
||||
b'-' => (true, &s[1..]),
|
||||
b'+' => (false, &s[1..]),
|
||||
_ => (false, s),
|
||||
};
|
||||
let (base, digits) = if rest.len() >= 2 && rest.as_bytes()[0] == b'0' {
|
||||
match rest.as_bytes()[1] {
|
||||
b'x' => (16, &rest[2..]),
|
||||
b'o' => (8, &rest[2..]),
|
||||
b'b' => (2, &rest[2..]),
|
||||
_ => (10, rest),
|
||||
}
|
||||
} else {
|
||||
(10, rest)
|
||||
};
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let db = digits.as_bytes();
|
||||
if db[0] == b'_' || db[db.len() - 1] == b'_' || digits.contains("__") {
|
||||
return None;
|
||||
}
|
||||
let mut acc: i128 = 0;
|
||||
for &c in db {
|
||||
if c == b'_' {
|
||||
continue;
|
||||
}
|
||||
let d = i128::from((c as char).to_digit(base)?);
|
||||
acc = acc.checked_mul(i128::from(base))?.checked_add(d)?;
|
||||
}
|
||||
Some(if neg { -acc } else { acc })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ConfigField impls for primitives
|
||||
|
||||
@@ -580,10 +580,9 @@ macro_rules! impl_int_field {
|
||||
}
|
||||
|
||||
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
||||
let value = crate::cli_args::text_value(value)?;
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
let n = parse_int_base0(v).ok_or(ArgError::InvalidValue)?;
|
||||
*self = <$t>::try_from(n).map_err(|_| ArgError::InvalidValue)?;
|
||||
*self = ghostty_os::zig_fmt::parse_int::<$t>(v, 0)
|
||||
.map_err(|_| ArgError::InvalidValue)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -615,14 +614,14 @@ macro_rules! impl_float_field {
|
||||
}
|
||||
|
||||
fn parse_bytes(&mut self, value: Option<&[u8]>) -> Result<(), ArgError> {
|
||||
let value = crate::cli_args::text_value(value)?;
|
||||
let v = value.ok_or(ArgError::ValueRequired)?;
|
||||
*self = v.parse::<$t>().map_err(|_| ArgError::InvalidValue)?;
|
||||
*self = ghostty_os::zig_fmt::parse_float::<$t>(v)
|
||||
.map_err(|_| ArgError::InvalidValue)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
writeln!(f.writer, "{} = {}", f.name, self)
|
||||
writeln!(f.writer, "{} = {}", f.name, ghostty_os::zig_fmt::format_float_d(*self))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
@@ -1642,12 +1641,45 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_int_base0_forms() {
|
||||
assert_eq!(parse_int_base0("0x_1"), None);
|
||||
assert_eq!(parse_int_base0("1_000"), Some(1000));
|
||||
assert_eq!(parse_int_base0("-0b11"), Some(-3));
|
||||
assert_eq!(parse_int_base0("0o17"), Some(15));
|
||||
assert_eq!(parse_int_base0(""), None);
|
||||
fn numeric_fields_use_zig_parse_int_and_parse_float_grammars() {
|
||||
let mut n = 0u32;
|
||||
n.parse_bytes(Some(b"0X10")).unwrap();
|
||||
assert_eq!(n, 16);
|
||||
n.parse_bytes(Some(b"1__0")).unwrap();
|
||||
assert_eq!(n, 10);
|
||||
n.parse_bytes(Some(b"+0b1_1")).unwrap();
|
||||
assert_eq!(n, 3);
|
||||
for bad in ["0x_1", "_1", "1_", "-1", "0x", "", " 1", "1 ", "\u{e9}"] {
|
||||
assert_eq!(
|
||||
n.parse_bytes(Some(bad.as_bytes())),
|
||||
Err(ArgError::InvalidValue),
|
||||
"{bad:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(n.parse_bytes(None), Err(ArgError::ValueRequired));
|
||||
let mut i = 0i16;
|
||||
i.parse_bytes(Some(b"-0O17")).unwrap();
|
||||
assert_eq!(i, -15);
|
||||
assert_eq!(i.parse_bytes(Some(b"40000")), Err(ArgError::InvalidValue));
|
||||
// Invalid UTF-8 is just an invalid character for numbers.
|
||||
assert_eq!(n.parse_bytes(Some(b"\xff")), Err(ArgError::InvalidValue));
|
||||
|
||||
let mut f = 0f32;
|
||||
f.parse_bytes(Some(b"1_4")).unwrap();
|
||||
assert_eq!(f, 14.0);
|
||||
f.parse_bytes(Some(b"0x1.cp+3")).unwrap();
|
||||
assert_eq!(f, 14.0);
|
||||
f.parse_bytes(Some(b"-INF")).unwrap();
|
||||
assert_eq!(f, f32::NEG_INFINITY);
|
||||
f.parse_bytes(Some(b"nan")).unwrap();
|
||||
assert!(f.is_nan());
|
||||
for bad in ["1__4", "infx", ".", "1e", " 1", "0x"] {
|
||||
assert_eq!(
|
||||
f.parse_bytes(Some(bad.as_bytes())),
|
||||
Err(ArgError::InvalidValue),
|
||||
"{bad:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user